Merge pull request #2118 from RoboSats/use-orbot-button

Use Orbot button
This commit is contained in:
KoalaSat
2025-07-27 11:59:23 +00:00
committed by GitHub
12 changed files with 84 additions and 70 deletions

View File

@ -23,7 +23,9 @@ import android.webkit.WebViewClient
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.widget.Button
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@ -42,6 +44,8 @@ class MainActivity : AppCompatActivity() {
private lateinit var loadingContainer: ConstraintLayout private lateinit var loadingContainer: ConstraintLayout
private lateinit var statusTextView: TextView private lateinit var statusTextView: TextView
private lateinit var intentData: String private lateinit var intentData: String
private lateinit var useOrbotButton: Button
var useProxy: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -58,13 +62,16 @@ class MainActivity : AppCompatActivity() {
webView = findViewById(R.id.webView) webView = findViewById(R.id.webView)
loadingContainer = findViewById(R.id.loadingContainer) loadingContainer = findViewById(R.id.loadingContainer)
statusTextView = findViewById(R.id.statusTextView) statusTextView = findViewById(R.id.statusTextView)
useOrbotButton = findViewById(R.id.useOrbotButton)
// Set click listener for action button
useOrbotButton.setOnClickListener {
onUseOrbotButtonClicked()
}
// Set initial status message // Set initial status message
updateStatus("Initializing Tor connection...") updateStatus("Initializing Tor connection...")
// Initialize Tor and setup WebView only after Tor is properly connected
initializeTor()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
this, this,
@ -85,6 +92,15 @@ class MainActivity : AppCompatActivity() {
intentData = orderId intentData = orderId
} }
} }
// Initialize Tor and setup WebView only after Tor is properly connected
initializeTor()
val settingProxy = EncryptedStorage.getEncryptedStorage("settings_use_proxy")
if (settingProxy == "false") {
// Setup WebView to use Orbot if the user previously clicked
onUseOrbotButtonClicked()
}
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@ -97,6 +113,26 @@ class MainActivity : AppCompatActivity() {
} }
} }
/**
* Disables the built-in proxy for users with Orbot configured
* This assumes that Orbot is already running and properly configured
* to handle .onion addresses through the system proxy settings
*/
private fun onUseOrbotButtonClicked() {
Log.d("OrbotMode", "Switching to Orbot proxy mode")
EncryptedStorage.setEncryptedStorage("settings_use_proxy", "false")
useProxy = false
// Show a message to the user
Toast.makeText(
this,
"Using Orbot. Make sure it's running!",
Toast.LENGTH_LONG
).show()
setupWebView()
}
/** /**
* Initialize Notifications service * Initialize Notifications service
*/ */
@ -127,7 +163,7 @@ class MainActivity : AppCompatActivity() {
private fun initializeTor() { private fun initializeTor() {
try { try {
try { try {
torKmp = TorKmpManager.getTorKmpObject() torKmp = getTorKmpObject()
} catch (e: UninitializedPropertyAccessException) { } catch (e: UninitializedPropertyAccessException) {
torKmp = TorKmp(application as Application) torKmp = TorKmp(application as Application)
TorKmpManager.updateTorKmpObject(torKmp) TorKmpManager.updateTorKmpObject(torKmp)
@ -218,19 +254,25 @@ class MainActivity : AppCompatActivity() {
*/ */
private fun setupWebView() { private fun setupWebView() {
// Double-check Tor is connected before proceeding // Double-check Tor is connected before proceeding
if (!torKmp.isConnected()) { if (useProxy && !torKmp.isConnected()) {
Log.e("SecurityError", "Attempted to set up WebView without Tor connection") Log.e("SecurityError", "Attempted to set up WebView without Tor connection")
return return
} }
// Set a blocking WebViewClient to prevent ANY network access // Set a blocking WebViewClient to prevent ANY network access
webView.webViewClient = object : WebViewClient() { webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
// Block ALL requests until we're sure Tor proxy is correctly set up // Block ALL requests until we're sure Tor proxy is correctly set up
return WebResourceResponse("text/plain", "UTF-8", null) return WebResourceResponse("text/plain", "UTF-8", null)
} }
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
// Block ALL URL loading attempts // Block ALL URL loading attempts
return true return true
} }
@ -241,23 +283,17 @@ class MainActivity : AppCompatActivity() {
// Show message that we're setting up secure browsing // Show message that we're setting up secure browsing
runOnUiThread { runOnUiThread {
updateStatus("Setting up secure Tor browsing...") updateStatus(if (useProxy) "Setting up secure Tor browsing..." else "Setting up Orbot browsing...")
} }
// Configure proxy for WebView in a background thread to avoid NetworkOnMainThreadException // Configure proxy for WebView in a background thread to avoid NetworkOnMainThreadException
Thread { Thread {
try { try {
// First verify Tor is still connected // First verify Tor is still connected
if (!torKmp.isConnected()) { if (useProxy && !torKmp.isConnected()) {
throw SecurityException("Tor disconnected during proxy setup") throw SecurityException("Tor disconnected during proxy setup")
} }
// If we get here, proxy setup was successful
// Perform one final Tor connection check
if (!torKmp.isConnected()) {
throw SecurityException("Tor disconnected after proxy setup")
}
// Success - now configure WebViewClient and load URL on UI thread // Success - now configure WebViewClient and load URL on UI thread
runOnUiThread { runOnUiThread {
updateStatus("Secure connection established. Loading app...") updateStatus("Secure connection established. Loading app...")

View File

@ -14,7 +14,6 @@ import com.robosats.tor.TorKmpManager.getTorKmpObject
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.OkHttpClient.Builder import okhttp3.OkHttpClient.Builder
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -35,7 +34,7 @@ import okhttp3.Request.Builder as RequestBuilder
* sanitization, and proper error handling. * sanitization, and proper error handling.
*/ */
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
class WebAppInterface(private val context: Context, private val webView: WebView) { class WebAppInterface(private val context: MainActivity, private val webView: WebView) {
private val TAG = "WebAppInterface" private val TAG = "WebAppInterface"
private val roboIdentities = RoboIdentities() private val roboIdentities = RoboIdentities()
private val webSockets: MutableMap<String?, WebSocket?> = HashMap<String?, WebSocket?>() private val webSockets: MutableMap<String?, WebSocket?> = HashMap<String?, WebSocket?>()
@ -170,12 +169,16 @@ class WebAppInterface(private val context: Context, private val webView: WebView
try { try {
Log.d(TAG, "WebSocket opening: $path") Log.d(TAG, "WebSocket opening: $path")
val client: OkHttpClient = Builder() // Create OkHttpClient
var builder = Builder()
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout .readTimeout(120, TimeUnit.SECONDS) // Set read timeout
.proxy(getTorKmpObject().proxy)
.build()
if (context.useProxy) {
builder = builder.proxy(getTorKmpObject().proxy)
}
val client = builder.build()
// Create a request for the WebSocket connection // Create a request for the WebSocket connection
val request: Request = RequestBuilder() val request: Request = RequestBuilder()
@ -258,12 +261,16 @@ class WebAppInterface(private val context: Context, private val webView: WebView
} }
try { try {
// Create OkHttpClient with Tor proxy // Create OkHttpClient
val client = Builder() var builder = Builder()
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout .readTimeout(120, TimeUnit.SECONDS) // Set read timeout
.proxy(getTorKmpObject().proxy)
.build() if (context.useProxy) {
builder = builder.proxy(getTorKmpObject().proxy)
}
val client = builder.build()
// Build request with URL // Build request with URL
val requestBuilder = RequestBuilder().url(url) val requestBuilder = RequestBuilder().url(url)

View File

@ -68,7 +68,16 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loadingProgressBar" /> app:layout_constraintTop_toBottomOf="@+id/loadingProgressBar" />
<Button
android:id="@+id/useOrbotButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/useOrbotButton"
android:textColor="#FFFFFF"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,4 +5,5 @@
<string name="configuration">Configuration</string> <string name="configuration">Configuration</string>
<string name="robosats_is_running_in_background">Robosats is running in background fetching for notifications</string> <string name="robosats_is_running_in_background">Robosats is running in background fetching for notifications</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="useOrbotButton">Use Orbot</string>
</resources> </resources>

View File

@ -43,7 +43,7 @@ const MenuDrawer = ({ show, setShow }: MenuDrawerProps): React.JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const { garage } = useContext<UseGarageStoreType>(GarageContext); const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { open, setOpen, client, torStatus, page, navigateToPage } = const { open, setOpen, client, torStatus, page, navigateToPage, settings } =
useContext<UseAppStoreType>(AppContext); useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext); const { federation } = useContext<UseFederationStoreType>(FederationContext);
const [openGarage, setOpenGarage] = useState<boolean>(false); const [openGarage, setOpenGarage] = useState<boolean>(false);
@ -242,28 +242,14 @@ const MenuDrawer = ({ show, setShow }: MenuDrawerProps): React.JSX.Element => {
<ListItemText primary={t('Settings')} /> <ListItemText primary={t('Settings')} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
{client === 'mobile' && ( {client === 'mobile' && settings.useProxy && (
<ListItem disablePadding sx={{ display: 'flex', flexDirection: 'column' }}> <ListItem disablePadding sx={{ display: 'flex', flexDirection: 'column' }}>
<ListItemButton selected> <ListItemButton selected>
<ListItemIcon> <ListItemIcon>
{torProgress ? ( {torProgress ? (
<> <Box>
<CircularProgress color={torColor} thickness={6} size={22} /> <CircularProgress color={torColor} thickness={6} size={22} />
<Box </Box>
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<TorIcon color={torColor} sx={{ width: 20, height: 20 }} />
</Box>
</>
) : ( ) : (
<Box> <Box>
<TorIcon color={torColor} sx={{ width: 20, height: 20 }} /> <TorIcon color={torColor} sx={{ width: 20, height: 20 }} />

View File

@ -1,7 +1,5 @@
import i18n from '../i18n/Web'; import i18n from '../i18n/Web';
import { systemClient } from '../services/System'; import { systemClient } from '../services/System';
import { websocketClient } from '../services/Websocket';
import { apiClient } from '../services/api';
import { getHost } from '../utils'; import { getHost } from '../utils';
export type Language = export type Language =
@ -65,8 +63,6 @@ class BaseSettings {
systemClient.getItem('settings_use_proxy').then((result) => { systemClient.getItem('settings_use_proxy').then((result) => {
this.useProxy = client === 'mobile' && result !== 'false'; this.useProxy = client === 'mobile' && result !== 'false';
apiClient.useProxy = this.useProxy;
websocketClient.useProxy = this.useProxy;
}); });
} }

View File

@ -47,13 +47,9 @@ class WebsocketConnectionAndroid implements WebsocketConnection {
} }
class WebsocketAndroidClient implements WebsocketClient { class WebsocketAndroidClient implements WebsocketClient {
public useProxy = true;
private readonly webClient: WebsocketWebClient = new WebsocketWebClient(); private readonly webClient: WebsocketWebClient = new WebsocketWebClient();
public open: (path: string) => Promise<WebsocketConnection> = async (path) => { public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
if (!this.useProxy) return await this.webClient.open(path);
return new Promise<WebsocketConnectionAndroid>((resolve, reject) => { return new Promise<WebsocketConnectionAndroid>((resolve, reject) => {
const uuid: string = uuidv4(); const uuid: string = uuidv4();
window.AndroidAppRobosats?.openWS(uuid, path); window.AndroidAppRobosats?.openWS(uuid, path);

View File

@ -39,8 +39,6 @@ class WebsocketConnectionWeb implements WebsocketConnection {
} }
class WebsocketWebClient implements WebsocketClient { class WebsocketWebClient implements WebsocketClient {
public useProxy = false;
public open: (path: string) => Promise<WebsocketConnection> = async (path) => { public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
return await new Promise<WebsocketConnection>((resolve, reject) => { return await new Promise<WebsocketConnection>((resolve, reject) => {
try { try {

View File

@ -18,7 +18,6 @@ export interface WebsocketConnection {
} }
export interface WebsocketClient { export interface WebsocketClient {
useProxy: boolean;
open: (path: string) => Promise<WebsocketConnection>; open: (path: string) => Promise<WebsocketConnection>;
} }

View File

@ -1,12 +1,7 @@
import { type ApiClient, type Auth } from '..'; import { type ApiClient, type Auth } from '..';
import ApiWebClient from '../ApiWebClient';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
class ApiAndroidClient implements ApiClient { class ApiAndroidClient implements ApiClient {
public useProxy = true;
private readonly webClient: ApiClient = new ApiWebClient();
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => { private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
let headers = { let headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -47,8 +42,6 @@ class ApiAndroidClient implements ApiClient {
public delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> = public delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> =
async (baseUrl, path, auth) => { async (baseUrl, path, auth) => {
if (!this.useProxy) return await this.webClient.delete(baseUrl, path, auth);
const jsonHeaders = JSON.stringify(this.getHeaders(auth)); const jsonHeaders = JSON.stringify(this.getHeaders(auth));
const result = await new Promise<string>((resolve, reject) => { const result = await new Promise<string>((resolve, reject) => {
@ -66,8 +59,6 @@ class ApiAndroidClient implements ApiClient {
body: object, body: object,
auth?: Auth, auth?: Auth,
) => Promise<object | undefined> = async (baseUrl, path, body, auth) => { ) => Promise<object | undefined> = async (baseUrl, path, body, auth) => {
if (!this.useProxy) return await this.webClient.post(baseUrl, path, body, auth);
const jsonHeaders = JSON.stringify(this.getHeaders(auth)); const jsonHeaders = JSON.stringify(this.getHeaders(auth));
const jsonBody = JSON.stringify(body); const jsonBody = JSON.stringify(body);
@ -85,8 +76,6 @@ class ApiAndroidClient implements ApiClient {
path, path,
auth, auth,
) => { ) => {
if (!this.useProxy) return await this.webClient.get(baseUrl, path, auth);
const jsonHeaders = JSON.stringify(this.getHeaders(auth)); const jsonHeaders = JSON.stringify(this.getHeaders(auth));
const result = await new Promise<string>((resolve, reject) => { const result = await new Promise<string>((resolve, reject) => {

View File

@ -1,8 +1,6 @@
import { type ApiClient, type Auth } from '..'; import { type ApiClient, type Auth } from '..';
class ApiWebClient implements ApiClient { class ApiWebClient implements ApiClient {
public useProxy = false;
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => { private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
let headers = { let headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -8,7 +8,6 @@ export interface Auth {
} }
export interface ApiClient { export interface ApiClient {
useProxy: boolean;
post: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>; post: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>;
put: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>; put: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>;
get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>; get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;