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.pm.PackageManager
import android.net.Uri
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
@ -42,6 +44,8 @@ class MainActivity : AppCompatActivity() {
private lateinit var loadingContainer: ConstraintLayout
private lateinit var statusTextView: TextView
private lateinit var intentData: String
private lateinit var useOrbotButton: Button
var useProxy: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -58,13 +62,16 @@ class MainActivity : AppCompatActivity() {
webView = findViewById(R.id.webView)
loadingContainer = findViewById(R.id.loadingContainer)
statusTextView = findViewById(R.id.statusTextView)
useOrbotButton = findViewById(R.id.useOrbotButton)
// Set click listener for action button
useOrbotButton.setOnClickListener {
onUseOrbotButtonClicked()
}
// Set initial status message
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 &&
ContextCompat.checkSelfPermission(
this,
@ -85,6 +92,15 @@ class MainActivity : AppCompatActivity() {
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) {
@ -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
*/
@ -127,7 +163,7 @@ class MainActivity : AppCompatActivity() {
private fun initializeTor() {
try {
try {
torKmp = TorKmpManager.getTorKmpObject()
torKmp = getTorKmpObject()
} catch (e: UninitializedPropertyAccessException) {
torKmp = TorKmp(application as Application)
TorKmpManager.updateTorKmpObject(torKmp)
@ -218,19 +254,25 @@ class MainActivity : AppCompatActivity() {
*/
private fun setupWebView() {
// 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")
return
}
// Set a blocking WebViewClient to prevent ANY network access
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
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
return true
}
@ -241,23 +283,17 @@ class MainActivity : AppCompatActivity() {
// Show message that we're setting up secure browsing
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
Thread {
try {
// First verify Tor is still connected
if (!torKmp.isConnected()) {
if (useProxy && !torKmp.isConnected()) {
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
runOnUiThread {
updateStatus("Secure connection established. Loading app...")

View File

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

View File

@ -68,7 +68,16 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
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>

View File

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

View File

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

View File

@ -1,7 +1,5 @@
import i18n from '../i18n/Web';
import { systemClient } from '../services/System';
import { websocketClient } from '../services/Websocket';
import { apiClient } from '../services/api';
import { getHost } from '../utils';
export type Language =
@ -65,8 +63,6 @@ class BaseSettings {
systemClient.getItem('settings_use_proxy').then((result) => {
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 {
public useProxy = true;
private readonly webClient: WebsocketWebClient = new WebsocketWebClient();
public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
if (!this.useProxy) return await this.webClient.open(path);
return new Promise<WebsocketConnectionAndroid>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.openWS(uuid, path);

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@ export interface Auth {
}
export interface ApiClient {
useProxy: boolean;
post: (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>;