Merge cc7f867e44ea33e0036a716e46d7134848a4df94 into 35cb618ec76c58816177aeda6c93b1c8769489af
@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
import { type Page } from '../../basic/NavBar';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { getSettings } from '../../contexts/AppContext';
|
||||
|
||||
interface NotificationsProps {
|
||||
rewards: number | undefined;
|
||||
@ -30,9 +31,9 @@ interface NotificationMessage {
|
||||
}
|
||||
|
||||
const path =
|
||||
window.NativeRobosats === undefined
|
||||
? '/static/assets/sounds'
|
||||
: 'file:///android_asset/Web.bundle/assets/sounds';
|
||||
getSettings().client == 'mobile'
|
||||
? 'file:///android_asset/Web.bundle/assets/sounds'
|
||||
: '/static/assets/sounds';
|
||||
|
||||
const audio = {
|
||||
chat: new Audio(`${path}/chat-open.mp3`),
|
||||
|
@ -20,11 +20,12 @@ import {
|
||||
type UseFederationStoreType,
|
||||
FederationContext,
|
||||
} from '../../../../contexts/FederationContext';
|
||||
import { getSettings } from '../../../../contexts/AppContext';
|
||||
|
||||
const audioPath =
|
||||
window.NativeRobosats === undefined
|
||||
? '/static/assets/sounds'
|
||||
: 'file:///android_asset/Web.bundle/assets/sounds';
|
||||
getSettings().client == 'mobile'
|
||||
? 'file:///android_asset/Web.bundle/assets/sounds'
|
||||
: '/static/assets/sounds';
|
||||
|
||||
interface Props {
|
||||
order: Order;
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
} from '../../../../contexts/FederationContext';
|
||||
import { type UseGarageStoreType, GarageContext } from '../../../../contexts/GarageContext';
|
||||
import { type Order } from '../../../../models';
|
||||
import { getSettings } from '../../../../contexts/AppContext';
|
||||
|
||||
interface Props {
|
||||
order: Order;
|
||||
@ -35,9 +36,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const audioPath =
|
||||
window.NativeRobosats === undefined
|
||||
? '/static/assets/sounds'
|
||||
: 'file:///android_asset/Web.bundle/assets/sounds';
|
||||
getSettings().client == 'mobile'
|
||||
? 'file:///android_asset/Web.bundle/assets/sounds'
|
||||
: '/static/assets/sounds';
|
||||
|
||||
const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
order,
|
||||
|
@ -36,13 +36,8 @@ export interface SlideDirection {
|
||||
|
||||
export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF';
|
||||
|
||||
export const isNativeRoboSats = !(window.NativeRobosats === undefined);
|
||||
|
||||
const pageFromPath = window.location.pathname.split('/')[1];
|
||||
const isPagePathEmpty = pageFromPath === '';
|
||||
const entryPage: Page = !isNativeRoboSats
|
||||
? ((isPagePathEmpty ? 'garage' : pageFromPath) as Page)
|
||||
: 'garage';
|
||||
|
||||
export const closeAll: OpenDialogs = {
|
||||
more: false,
|
||||
@ -56,6 +51,7 @@ export const closeAll: OpenDialogs = {
|
||||
update: false,
|
||||
profile: false,
|
||||
recovery: false,
|
||||
thirdParty: '',
|
||||
};
|
||||
|
||||
const makeTheme = function (settings: Settings): Theme {
|
||||
@ -108,7 +104,7 @@ const getOrigin = (network = 'mainnet'): Origin => {
|
||||
return origin;
|
||||
};
|
||||
|
||||
const getSettings = (): Settings => {
|
||||
export const getSettings = (): Settings => {
|
||||
let settings;
|
||||
|
||||
const [client, view] = window.RobosatsSettings.split('-');
|
||||
@ -120,6 +116,11 @@ const getSettings = (): Settings => {
|
||||
return settings;
|
||||
};
|
||||
|
||||
const entryPage: Page =
|
||||
getSettings().client == 'mobile'
|
||||
? 'garage'
|
||||
: ((isPagePathEmpty ? 'garage' : pageFromPath) as Page);
|
||||
|
||||
export interface WindowSize {
|
||||
width: number;
|
||||
height: number;
|
||||
@ -159,7 +160,7 @@ export interface UseAppStoreType {
|
||||
|
||||
export const initialAppContext: UseAppStoreType = {
|
||||
theme: undefined,
|
||||
torStatus: 'STARTING',
|
||||
torStatus: 'ON',
|
||||
settings: getSettings(),
|
||||
setSettings: () => {},
|
||||
page: entryPage,
|
||||
|
@ -51,6 +51,7 @@ class BaseSettings {
|
||||
this.host = getHost();
|
||||
|
||||
const [client] = window.RobosatsSettings.split('-');
|
||||
this.client = client;
|
||||
|
||||
const stopNotifications = systemClient.getItem('settings_stop_notifications');
|
||||
this.stopNotifications = client === 'mobile' && stopNotifications === 'true';
|
||||
@ -63,6 +64,7 @@ class BaseSettings {
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'basic';
|
||||
public mode: 'light' | 'dark' = 'light';
|
||||
public client: 'web' | 'mobile' = 'web';
|
||||
public fontSize: number = 14;
|
||||
public lightQRs: boolean = false;
|
||||
public language?: Language;
|
||||
|
@ -236,6 +236,60 @@ const configMobile: Configuration = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
|
||||
templateParameters: {
|
||||
pro: false,
|
||||
},
|
||||
filename: path.resolve(__dirname, '../mobile_new/app/src/main/assets/index.html'),
|
||||
inject: 'body',
|
||||
robosatsSettings: 'mobile-basic',
|
||||
basePath: 'file:///android_asset/Web.bundle/',
|
||||
}),
|
||||
new FileManagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
copy: [
|
||||
{
|
||||
source: path.resolve(__dirname, 'static/css'),
|
||||
destination: path.resolve(
|
||||
__dirname,
|
||||
'../mobile_new/app/src/main/assets/Web.bundle/static/css',
|
||||
),
|
||||
},
|
||||
{
|
||||
source: path.resolve(__dirname, 'static/assets/sounds'),
|
||||
destination: path.resolve(
|
||||
__dirname,
|
||||
'../mobile_new/app/src/main/assets/Web.bundle/assets/sounds',
|
||||
),
|
||||
},
|
||||
{
|
||||
source: path.resolve(__dirname, 'static/federation'),
|
||||
destination: path.resolve(
|
||||
__dirname,
|
||||
'../mobile_new/app/src/main/assets/Web.bundle/assets/federation',
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
new FileManagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
copy: [
|
||||
{
|
||||
source: path.resolve(__dirname, '../mobile/html/Web.bundle/static/frontend'),
|
||||
destination: path.resolve(
|
||||
__dirname,
|
||||
'../mobile_new/app/src/main/assets/static/frontend',
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
|
15
mobile_new/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
2
mobile_new/app/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/build
|
||||
/src/main/assets/*
|
81
mobile_new/app/build.gradle.kts
Normal file
@ -0,0 +1,81 @@
|
||||
import com.android.build.api.dsl.Packaging
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.koalasat.robosats"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.koalasat.robosats"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
splits {
|
||||
|
||||
// Configures multiple APKs based on ABI. This helps keep the size
|
||||
// down, since PT binaries can be large.
|
||||
abi {
|
||||
|
||||
// Enables building multiple APKs per ABI.
|
||||
isEnable = true
|
||||
|
||||
// By default, all ABIs are included, so use reset() and include to specify
|
||||
// that we only want APKs for x86 and x86_64, armeabi-v7a, and arm64-v8a.
|
||||
|
||||
// Resets the list of ABIs that Gradle should create APKs for to none.
|
||||
reset()
|
||||
|
||||
// Specifies a list of ABIs that Gradle should create APKs for.
|
||||
include("x86", "armeabi-v7a", "arm64-v8a", "x86_64")
|
||||
|
||||
// Specify whether you wish to also generate a universal APK that
|
||||
// includes _all_ ABIs.
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
fun Packaging.() {
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.kmp.tor)
|
||||
// Add the KMP Tor binary dependency (contains the native .so files)
|
||||
implementation(libs.kmp.tor.binary)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
}
|
21
mobile_new/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,24 @@
|
||||
package com.koalasat.robosats
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.koalasat.robosats", appContext.packageName)
|
||||
}
|
||||
}
|
30
mobile_new/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:extractNativeLibs="true"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.Robosats">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,508 @@
|
||||
package com.koalasat.robosats
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.webkit.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.robosats.tor.TorKmp
|
||||
import com.robosats.tor.TorKmpManager
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var torKmp: TorKmp
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// We don't need edge-to-edge since we're using fitsSystemWindows
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// Set up the WebView reference
|
||||
webView = findViewById(R.id.webView)
|
||||
|
||||
// Initialize Tor and setup WebView only after Tor is properly connected
|
||||
initializeTor()
|
||||
}
|
||||
|
||||
private fun initializeTor() {
|
||||
// Initialize TorKmp if it's not already initialized
|
||||
try {
|
||||
try {
|
||||
torKmp = TorKmpManager.getTorKmpObject()
|
||||
} catch (e: UninitializedPropertyAccessException) {
|
||||
torKmp = TorKmp(application as Application)
|
||||
TorKmpManager.updateTorKmpObject(torKmp)
|
||||
torKmp.torOperationManager.startQuietly()
|
||||
}
|
||||
|
||||
// Run Tor connection check on a background thread
|
||||
Thread {
|
||||
waitForTorConnection()
|
||||
}.start()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Log the error and show a critical error message
|
||||
Log.e("TorInitialization", "Failed to initialize Tor: ${e.message}", e)
|
||||
|
||||
// Show a toast notification about the critical error
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Critical error: Tor initialization failed. App cannot proceed securely.",
|
||||
android.widget.Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForTorConnection() {
|
||||
var retries = 0
|
||||
val maxRetries = 15
|
||||
|
||||
try {
|
||||
// Display connecting message
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Connecting to Tor network...",
|
||||
android.widget.Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Wait for Tor to connect with retry mechanism
|
||||
while (!torKmp.isConnected() && retries < maxRetries) {
|
||||
if (!torKmp.isStarting()) {
|
||||
torKmp.torOperationManager.startQuietly()
|
||||
}
|
||||
Thread.sleep(2000)
|
||||
retries += 1
|
||||
|
||||
// Update status on UI thread every few retries
|
||||
if (retries % 3 == 0) {
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Still connecting to Tor (attempt $retries/$maxRetries)...",
|
||||
android.widget.Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Tor connected successfully
|
||||
if (torKmp.isConnected()) {
|
||||
Log.d("TorInitialization", "Tor connected successfully after $retries retries")
|
||||
|
||||
// Show success message
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Tor connected successfully",
|
||||
android.widget.Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
// Now that Tor is connected, set up the WebView
|
||||
setupWebView()
|
||||
}
|
||||
} else {
|
||||
// If we've exhausted retries and still not connected
|
||||
Log.e("TorInitialization", "Failed to connect to Tor after $maxRetries retries")
|
||||
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Failed to connect to Tor after multiple attempts. App cannot proceed securely.",
|
||||
android.widget.Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("TorInitialization", "Error during Tor connection: ${e.message}", e)
|
||||
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Error connecting to Tor: ${e.message}",
|
||||
android.widget.Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupWebView() {
|
||||
// Double-check Tor is connected before proceeding
|
||||
if (!torKmp.isConnected()) {
|
||||
Log.e("SecurityError", "Attempted to set up WebView without Tor connection")
|
||||
return
|
||||
}
|
||||
|
||||
// IMMEDIATELY set a blocking WebViewClient to prevent ANY network access
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
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 {
|
||||
// Block ALL URL loading attempts until proxy is properly configured
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Configure WebView settings on UI thread
|
||||
val webSettings = webView.settings
|
||||
|
||||
// Enable JavaScript
|
||||
webSettings.javaScriptEnabled = true
|
||||
|
||||
// Enable DOM storage for HTML5 apps
|
||||
webSettings.domStorageEnabled = true
|
||||
|
||||
// Enable CORS and cross-origin requests
|
||||
webSettings.allowUniversalAccessFromFileURLs = true
|
||||
webSettings.allowFileAccessFromFileURLs = true
|
||||
|
||||
// Disable cache completely to prevent leaks
|
||||
webSettings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
|
||||
// Enable mixed content (http in https)
|
||||
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
|
||||
// Enable zooming
|
||||
webSettings.setSupportZoom(true)
|
||||
webSettings.builtInZoomControls = true
|
||||
webSettings.displayZoomControls = false
|
||||
|
||||
// Enable HTML5 features
|
||||
webSettings.allowFileAccess = true
|
||||
webSettings.allowContentAccess = true
|
||||
webSettings.loadWithOverviewMode = true
|
||||
webSettings.useWideViewPort = true
|
||||
webSettings.setSupportMultipleWindows(true)
|
||||
webSettings.javaScriptCanOpenWindowsAutomatically = true
|
||||
|
||||
// Improve display for better Android integration
|
||||
webSettings.textZoom = 100 // Normal text zoom
|
||||
|
||||
// Show message that we're setting up secure browsing
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Setting up secure Tor browsing...",
|
||||
android.widget.Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Configure proxy for WebView in a background thread to avoid NetworkOnMainThreadException
|
||||
Thread {
|
||||
try {
|
||||
// First verify Tor is still connected
|
||||
if (!torKmp.isConnected()) {
|
||||
throw SecurityException("Tor disconnected during proxy setup")
|
||||
}
|
||||
|
||||
// Try to set up the proxy
|
||||
setupProxyForWebView()
|
||||
|
||||
// If we get here, proxy setup was successful
|
||||
// Perform one final Tor connection check
|
||||
if (!torKmp.isConnected()) {
|
||||
throw SecurityException("Tor disconnected after proxy setup")
|
||||
}
|
||||
|
||||
// Now get the proxy information that we previously verified in setupProxyForWebView
|
||||
// Use system properties that we've already set up and verified
|
||||
val proxyHost = System.getProperty("http.proxyHost")
|
||||
?: throw SecurityException("Missing proxy host in system properties")
|
||||
val proxyPort = System.getProperty("http.proxyPort")?.toIntOrNull()
|
||||
?: throw SecurityException("Missing or invalid proxy port in system properties")
|
||||
|
||||
Log.d("TorProxy", "Using proxy settings: $proxyHost:$proxyPort")
|
||||
|
||||
// Success - now configure WebViewClient and load URL on UI thread
|
||||
runOnUiThread {
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"Secure connection established",
|
||||
android.widget.Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
// Create a custom WebViewClient that forces all traffic through Tor
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||
// Verify Tor is connected before allowing any resource request
|
||||
if (!torKmp.isConnected()) {
|
||||
Log.e("SecurityError", "Tor disconnected during resource request")
|
||||
return WebResourceResponse("text/plain", "UTF-8", null)
|
||||
}
|
||||
|
||||
val urlString = request.url.toString()
|
||||
Log.d("TorProxy", "Intercepting request: $urlString")
|
||||
|
||||
try {
|
||||
// Special handling for .onion domains
|
||||
val isOnionDomain = urlString.contains(".onion")
|
||||
|
||||
// For .onion domains, we must use SOCKS proxy type
|
||||
val proxyType = if (isOnionDomain)
|
||||
java.net.Proxy.Type.SOCKS
|
||||
else
|
||||
java.net.Proxy.Type.HTTP
|
||||
|
||||
// Create a proxy instance for Tor with the appropriate type
|
||||
val torProxy = java.net.Proxy(
|
||||
proxyType,
|
||||
java.net.InetSocketAddress(proxyHost, proxyPort)
|
||||
)
|
||||
|
||||
if (isOnionDomain) {
|
||||
Log.d("TorProxy", "Handling .onion domain with SOCKS proxy: $urlString")
|
||||
}
|
||||
|
||||
// Create connection with proxy already configured
|
||||
val url = java.net.URL(urlString)
|
||||
val connection = url.openConnection(torProxy)
|
||||
|
||||
// Configure basic connection properties
|
||||
connection.connectTimeout = 60000 // Longer timeout for onion domains
|
||||
connection.readTimeout = 60000
|
||||
|
||||
if (connection is java.net.HttpURLConnection) {
|
||||
// Ensure no connection reuse to prevent proxy leaks
|
||||
connection.setRequestProperty("Connection", "close")
|
||||
|
||||
// Copy request headers
|
||||
request.requestHeaders.forEach { (key, value) ->
|
||||
connection.setRequestProperty(key, value)
|
||||
}
|
||||
|
||||
// Special handling for OPTIONS (CORS preflight) requests
|
||||
if (request.method == "OPTIONS") {
|
||||
// Handle preflight CORS request
|
||||
connection.requestMethod = "OPTIONS"
|
||||
connection.setRequestProperty("Access-Control-Request-Method",
|
||||
request.requestHeaders["Access-Control-Request-Method"] ?: "GET, POST, OPTIONS")
|
||||
connection.setRequestProperty("Access-Control-Request-Headers",
|
||||
request.requestHeaders["Access-Control-Request-Headers"] ?: "")
|
||||
} else {
|
||||
// Set request method for non-OPTIONS requests
|
||||
connection.requestMethod = request.method
|
||||
}
|
||||
|
||||
// Try to connect
|
||||
connection.connect()
|
||||
val responseCode = connection.responseCode
|
||||
|
||||
// Get content type
|
||||
val mimeType = connection.contentType ?: "text/plain"
|
||||
val encoding = connection.contentEncoding ?: "UTF-8"
|
||||
|
||||
Log.d("TorProxy", "Successfully proxied request to $url (HTTP ${connection.responseCode})")
|
||||
|
||||
// Get the correct input stream based on response code
|
||||
val inputStream = if (responseCode >= 400) {
|
||||
connection.errorStream ?: java.io.ByteArrayInputStream(byteArrayOf())
|
||||
} else {
|
||||
connection.inputStream
|
||||
}
|
||||
|
||||
// Create response headers map with CORS headers
|
||||
val responseHeaders = HashMap<String, String>()
|
||||
|
||||
// Add CORS headers
|
||||
responseHeaders["Access-Control-Allow-Origin"] = "*"
|
||||
responseHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
||||
responseHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept"
|
||||
responseHeaders["Access-Control-Allow-Credentials"] = "true"
|
||||
|
||||
// Copy original response headers
|
||||
for (i in 0 until connection.headerFields.size) {
|
||||
val key = connection.headerFields.keys.elementAtOrNull(i)
|
||||
if (key != null) {
|
||||
val value = connection.getHeaderField(key)
|
||||
if (value != null) {
|
||||
responseHeaders[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return proxied response with CORS headers
|
||||
return WebResourceResponse(
|
||||
mimeType,
|
||||
encoding,
|
||||
responseCode,
|
||||
"OK",
|
||||
responseHeaders,
|
||||
inputStream
|
||||
)
|
||||
} else {
|
||||
// For non-HTTP connections (rare)
|
||||
val inputStream = connection.getInputStream()
|
||||
Log.d("TorProxy", "Successfully established non-HTTP connection to $url")
|
||||
return WebResourceResponse(
|
||||
"application/octet-stream",
|
||||
"UTF-8",
|
||||
inputStream
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("TorProxy", "Error proxying request: $urlString - ${e.message}", e)
|
||||
|
||||
// For non-onion domains, let the system handle it
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
// Verify Tor is still connected before allowing any request
|
||||
if (!torKmp.isConnected()) {
|
||||
Log.e("SecurityError", "Tor disconnected during navigation")
|
||||
return true // Block the request
|
||||
}
|
||||
return false // Let our proxied client handle it
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
|
||||
Log.e("WebViewError", "Error loading resource: ${error.description}")
|
||||
super.onReceivedError(view, request, error)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: android.graphics.Bitmap?) {
|
||||
// Verify Tor is connected when page starts loading
|
||||
if (!torKmp.isConnected()) {
|
||||
Log.e("SecurityError", "Tor disconnected as page started loading")
|
||||
view.stopLoading()
|
||||
return
|
||||
}
|
||||
super.onPageStarted(view, url, favicon)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
// Verify Tor is still connected when page finishes loading
|
||||
if (!torKmp.isConnected()) {
|
||||
Log.e("SecurityError", "Tor disconnected after page loaded")
|
||||
return
|
||||
}
|
||||
|
||||
// No JavaScript injection - just log page load completion
|
||||
Log.d("WebView", "Page finished loading: $url")
|
||||
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
}
|
||||
|
||||
// Now it's safe to load the local HTML file
|
||||
webView.loadUrl("file:///android_asset/index.html")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebViewSetup", "Security error in WebView setup: ${e.message}", e)
|
||||
|
||||
// Show error and exit - DO NOT LOAD WEBVIEW
|
||||
runOnUiThread {
|
||||
// Show toast with error
|
||||
android.widget.Toast.makeText(
|
||||
this,
|
||||
"SECURITY ERROR: Cannot set up secure browsing: ${e.message}",
|
||||
android.widget.Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun setupProxyForWebView() {
|
||||
// Triple-check Tor is connected
|
||||
if (!torKmp.isConnected()) {
|
||||
throw SecurityException("Cannot set up proxy - Tor is not connected")
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the proxy from TorKmpManager, handling possible exceptions
|
||||
val proxy = TorKmpManager.getTorKmpObject().proxy ?:
|
||||
throw SecurityException("Tor proxy is null despite Tor being connected")
|
||||
|
||||
val inetSocketAddress = proxy.address() as InetSocketAddress
|
||||
val host = inetSocketAddress.hostName
|
||||
val port = inetSocketAddress.port
|
||||
|
||||
if (host.isBlank() || port <= 0) {
|
||||
throw SecurityException("Invalid Tor proxy address: $host:$port")
|
||||
}
|
||||
|
||||
Log.d("WebViewProxy", "Setting up Tor proxy: $host:$port")
|
||||
|
||||
// Set up the proxy
|
||||
setWebViewProxy(applicationContext, host, port)
|
||||
|
||||
// Verify proxy was set correctly
|
||||
if (System.getProperty("http.proxyHost") != host ||
|
||||
System.getProperty("http.proxyPort") != port.toString()) {
|
||||
throw SecurityException("Proxy verification failed - system properties don't match expected values")
|
||||
}
|
||||
|
||||
Log.d("WebViewProxy", "Proxy setup completed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebViewProxy", "Error setting up proxy: ${e.message}", e)
|
||||
throw SecurityException("Failed to set up Tor proxy: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the proxy for WebView using the most direct approach that's known to work with Tor
|
||||
*/
|
||||
private fun setWebViewProxy(context: Context, proxyHost: String, proxyPort: Int) {
|
||||
try {
|
||||
// First set system properties (required as a foundation)
|
||||
System.setProperty("http.proxyHost", proxyHost)
|
||||
System.setProperty("http.proxyPort", proxyPort.toString())
|
||||
System.setProperty("https.proxyHost", proxyHost)
|
||||
System.setProperty("https.proxyPort", proxyPort.toString())
|
||||
System.setProperty("proxy.host", proxyHost)
|
||||
System.setProperty("proxy.port", proxyPort.toString())
|
||||
|
||||
Log.d("WebViewProxy", "Set system proxy properties")
|
||||
|
||||
// Create and apply a proxy at the application level
|
||||
val proxyClass = Class.forName("android.net.ProxyInfo")
|
||||
val proxyConstructor = proxyClass.getConstructor(String::class.java, Int::class.javaPrimitiveType, String::class.java)
|
||||
val proxyInfo = proxyConstructor.newInstance(proxyHost, proxyPort, null)
|
||||
|
||||
try {
|
||||
// Try to set global proxy through ConnectivityManager
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
|
||||
val setDefaultProxyMethod = connectivityManager.javaClass.getDeclaredMethod("setDefaultProxy", proxyClass)
|
||||
setDefaultProxyMethod.isAccessible = true
|
||||
setDefaultProxyMethod.invoke(connectivityManager, proxyInfo)
|
||||
Log.d("WebViewProxy", "Set proxy via ConnectivityManager")
|
||||
} catch (e: Exception) {
|
||||
Log.w("WebViewProxy", "Could not set proxy via ConnectivityManager: ${e.message}")
|
||||
}
|
||||
|
||||
// WebView operations must be run on the UI thread
|
||||
runOnUiThread {
|
||||
try {
|
||||
// Force WebView to use proxy via direct settings (must be on UI thread)
|
||||
webView.settings.javaClass.getDeclaredMethod("setHttpProxy", String::class.java, Int::class.javaPrimitiveType)
|
||||
?.apply { isAccessible = true }
|
||||
?.invoke(webView.settings, proxyHost, proxyPort)
|
||||
Log.d("WebViewProxy", "Applied proxy directly to WebView settings")
|
||||
} catch (e: Exception) {
|
||||
Log.w("WebViewProxy", "Could not set proxy directly on WebView settings: ${e.message}")
|
||||
// Continue - we'll rely on system properties and connection-level proxying
|
||||
}
|
||||
}
|
||||
|
||||
// Wait to ensure UI thread operations complete
|
||||
// This prevents race conditions with WebView operations
|
||||
Thread.sleep(500)
|
||||
|
||||
Log.d("WebViewProxy", "Proxy setup completed")
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebViewProxy", "Error setting WebView proxy", e)
|
||||
throw SecurityException("Failed to set WebView proxy: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.robosats.tor
|
||||
|
||||
enum class EnumTorState {
|
||||
STARTING,
|
||||
ON,
|
||||
STOPPING,
|
||||
OFF
|
||||
}
|
@ -0,0 +1,403 @@
|
||||
package com.robosats.tor
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.matthewnelson.kmp.tor.KmpTorLoaderAndroid
|
||||
import io.matthewnelson.kmp.tor.TorConfigProviderAndroid
|
||||
import io.matthewnelson.kmp.tor.common.address.*
|
||||
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig
|
||||
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Option.*
|
||||
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Setting.*
|
||||
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlInfoGet
|
||||
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal
|
||||
import io.matthewnelson.kmp.tor.controller.common.events.TorEvent
|
||||
import io.matthewnelson.kmp.tor.manager.TorManager
|
||||
import io.matthewnelson.kmp.tor.manager.TorServiceConfig
|
||||
import io.matthewnelson.kmp.tor.manager.common.TorControlManager
|
||||
import io.matthewnelson.kmp.tor.manager.common.TorOperationManager
|
||||
import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent
|
||||
import io.matthewnelson.kmp.tor.manager.common.state.isOff
|
||||
import io.matthewnelson.kmp.tor.manager.common.state.isOn
|
||||
import io.matthewnelson.kmp.tor.manager.common.state.isStarting
|
||||
import io.matthewnelson.kmp.tor.manager.common.state.isStopping
|
||||
import io.matthewnelson.kmp.tor.manager.R
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
class TorKmp(application : Application) {
|
||||
|
||||
private val TAG = "TorListener"
|
||||
|
||||
private val providerAndroid by lazy {
|
||||
object : TorConfigProviderAndroid(context = application) {
|
||||
override fun provide(): TorConfig {
|
||||
return TorConfig.Builder {
|
||||
// Set multiple ports for all of the things
|
||||
val dns = Ports.Dns()
|
||||
put(dns.set(AorDorPort.Value(PortProxy(9252))))
|
||||
put(dns.set(AorDorPort.Value(PortProxy(9253))))
|
||||
|
||||
val socks = Ports.Socks()
|
||||
put(socks.set(AorDorPort.Value(PortProxy(9254))))
|
||||
put(socks.set(AorDorPort.Value(PortProxy(9255))))
|
||||
|
||||
val http = Ports.HttpTunnel()
|
||||
put(http.set(AorDorPort.Value(PortProxy(9258))))
|
||||
put(http.set(AorDorPort.Value(PortProxy(9259))))
|
||||
|
||||
val trans = Ports.Trans()
|
||||
put(trans.set(AorDorPort.Value(PortProxy(9262))))
|
||||
put(trans.set(AorDorPort.Value(PortProxy(9263))))
|
||||
|
||||
// If a port (9263) is already taken (by ^^^^ trans port above)
|
||||
// this will take its place and "overwrite" the trans port entry
|
||||
// because port 9263 is taken.
|
||||
put(socks.set(AorDorPort.Value(PortProxy(9263))))
|
||||
|
||||
// Set Flags
|
||||
socks.setFlags(setOf(
|
||||
Ports.Socks.Flag.OnionTrafficOnly
|
||||
)).setIsolationFlags(setOf(
|
||||
Ports.IsolationFlag.IsolateClientAddr,
|
||||
)).set(AorDorPort.Value(PortProxy(9264)))
|
||||
put(socks)
|
||||
|
||||
// reset our socks object to defaults
|
||||
socks.setDefault()
|
||||
|
||||
// Not necessary, as if ControlPort is missing it will be
|
||||
// automatically added for you; but for demonstration purposes...
|
||||
// put(Ports.Control().set(AorDorPort.Auto))
|
||||
|
||||
// Use a UnixSocket instead of TCP for the ControlPort.
|
||||
//
|
||||
// A unix domain socket will always be preferred on Android
|
||||
// if neither Ports.Control or UnixSockets.Control are provided.
|
||||
put(UnixSockets.Control().set(FileSystemFile(
|
||||
workDir.builder {
|
||||
|
||||
// Put the file in the "data" directory
|
||||
// so that we avoid any directory permission
|
||||
// issues.
|
||||
//
|
||||
// Note that DataDirectory is automatically added
|
||||
// for you if it is not present in your provided
|
||||
// config. If you set a custom Path for it, you
|
||||
// should use it here.
|
||||
addSegment(DataDirectory.DEFAULT_NAME)
|
||||
|
||||
addSegment(UnixSockets.Control.DEFAULT_NAME)
|
||||
}
|
||||
)))
|
||||
|
||||
// Use a UnixSocket instead of TCP for the SocksPort.
|
||||
put(UnixSockets.Socks().set(FileSystemFile(
|
||||
workDir.builder {
|
||||
|
||||
// Put the file in the "data" directory
|
||||
// so that we avoid any directory permission
|
||||
// issues.
|
||||
//
|
||||
// Note that DataDirectory is automatically added
|
||||
// for you if it is not present in your provided
|
||||
// config. If you set a custom Path for it, you
|
||||
// should use it here.
|
||||
addSegment(DataDirectory.DEFAULT_NAME)
|
||||
|
||||
addSegment(UnixSockets.Socks.DEFAULT_NAME)
|
||||
}
|
||||
)))
|
||||
|
||||
// For Android, disabling & reducing connection padding is
|
||||
// advisable to minimize mobile data usage.
|
||||
put(ConnectionPadding().set(AorTorF.False))
|
||||
put(ConnectionPaddingReduced().set(TorF.True))
|
||||
|
||||
// Tor default is 24h. Reducing to 10 min helps mitigate
|
||||
// unnecessary mobile data usage.
|
||||
put(DormantClientTimeout().set(Time.Minutes(10)))
|
||||
|
||||
// Tor defaults this setting to false which would mean if
|
||||
// Tor goes dormant, the next time it is started it will still
|
||||
// be in the dormant state and will not bootstrap until being
|
||||
// set to "active". This ensures that if it is a fresh start,
|
||||
// dormancy will be cancelled automatically.
|
||||
put(DormantCanceledByStartup().set(TorF.True))
|
||||
|
||||
// If planning to use v3 Client Authentication in a persistent
|
||||
// manner (where private keys are saved to disk via the "Persist"
|
||||
// flag), this is needed to be set.
|
||||
put(ClientOnionAuthDir().set(FileSystemDir(
|
||||
workDir.builder { addSegment(ClientOnionAuthDir.DEFAULT_NAME) }
|
||||
)))
|
||||
|
||||
val hsPath = workDir.builder {
|
||||
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
|
||||
addSegment("test_service")
|
||||
}
|
||||
// Add Hidden services
|
||||
put(HiddenService()
|
||||
.setPorts(ports = setOf(
|
||||
// Use a unix domain socket to communicate via IPC instead of over TCP
|
||||
HiddenService.UnixSocket(virtualPort = Port(80), targetUnixSocket = hsPath.builder {
|
||||
addSegment(HiddenService.UnixSocket.DEFAULT_UNIX_SOCKET_NAME)
|
||||
}),
|
||||
))
|
||||
.setMaxStreams(maxStreams = HiddenService.MaxStreams(value = 2))
|
||||
.setMaxStreamsCloseCircuit(value = TorF.True)
|
||||
.set(FileSystemDir(path = hsPath))
|
||||
)
|
||||
|
||||
put(HiddenService()
|
||||
.setPorts(ports = setOf(
|
||||
HiddenService.Ports(virtualPort = Port(80), targetPort = Port(1030)), // http
|
||||
HiddenService.Ports(virtualPort = Port(443), targetPort = Port(1030)) // https
|
||||
))
|
||||
.set(FileSystemDir(path =
|
||||
workDir.builder {
|
||||
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
|
||||
addSegment("test_service_2")
|
||||
}
|
||||
))
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val loaderAndroid by lazy {
|
||||
KmpTorLoaderAndroid(provider = providerAndroid)
|
||||
}
|
||||
|
||||
private val manager: TorManager by lazy {
|
||||
TorManager.newInstance(application = application, loader = loaderAndroid, requiredEvents = null)
|
||||
}
|
||||
|
||||
// only expose necessary interfaces
|
||||
val torOperationManager: TorOperationManager get() = manager
|
||||
val torControlManager: TorControlManager get() = manager
|
||||
|
||||
private val listener = TorListener()
|
||||
|
||||
val events: LiveData<String> get() = listener.eventLines
|
||||
|
||||
private val appScope by lazy {
|
||||
CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
|
||||
}
|
||||
|
||||
val torStateLiveData: MutableLiveData<TorState> = MutableLiveData()
|
||||
get() = field
|
||||
var torState: TorState = TorState()
|
||||
get() = field
|
||||
|
||||
var proxy: Proxy? = null
|
||||
get() = field
|
||||
|
||||
init {
|
||||
manager.debug(true)
|
||||
manager.addListener(listener)
|
||||
listener.addLine(TorServiceConfig.getMetaData(application).toString())
|
||||
}
|
||||
|
||||
fun isConnected(): Boolean {
|
||||
return manager.state.isOn() && manager.state.bootstrap >= 100
|
||||
}
|
||||
|
||||
fun isStarting(): Boolean {
|
||||
return manager.state.isStarting() ||
|
||||
(manager.state.isOn() && manager.state.bootstrap < 100);
|
||||
}
|
||||
|
||||
|
||||
fun newIdentity(appContext: Application) {
|
||||
appScope.launch {
|
||||
val result = manager.signal(TorControlSignal.Signal.NewNym)
|
||||
result.onSuccess {
|
||||
if (it !is String) {
|
||||
listener.addLine(TorControlSignal.NEW_NYM_SUCCESS)
|
||||
Toast.makeText(appContext, TorControlSignal.NEW_NYM_SUCCESS, Toast.LENGTH_SHORT).show()
|
||||
return@onSuccess
|
||||
}
|
||||
|
||||
val post: String? = when {
|
||||
it.startsWith(TorControlSignal.NEW_NYM_RATE_LIMITED) -> {
|
||||
// Rate limiting NEWNYM request: delaying by 8 second(s)
|
||||
val seconds: Int? = it.drop(TorControlSignal.NEW_NYM_RATE_LIMITED.length)
|
||||
.substringBefore(' ')
|
||||
.toIntOrNull()
|
||||
|
||||
if (seconds == null) {
|
||||
it
|
||||
} else {
|
||||
appContext.getString(
|
||||
R.string.kmp_tor_newnym_rate_limited,
|
||||
seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
it == TorControlSignal.NEW_NYM_SUCCESS -> {
|
||||
appContext.getString(R.string.kmp_tor_newnym_success)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (post != null) {
|
||||
listener.addLine(post)
|
||||
Toast.makeText(appContext, post, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
result.onFailure {
|
||||
val msg = "Tor identity change failed"
|
||||
listener.addLine(msg)
|
||||
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class TorListener: TorManagerEvent.Listener() {
|
||||
private val _eventLines: MutableLiveData<String> = MutableLiveData("")
|
||||
val eventLines: LiveData<String> = _eventLines
|
||||
private val events: MutableList<String> = ArrayList(50)
|
||||
fun addLine(line: String) {
|
||||
synchronized(this) {
|
||||
if (events.size > 49) {
|
||||
events.removeAt(0)
|
||||
}
|
||||
events.add(line)
|
||||
//Log.i(TAG, line)
|
||||
//_eventLines.value = events.joinToString("\n")
|
||||
_eventLines.postValue(events.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvent(event: TorManagerEvent) {
|
||||
|
||||
if (event is TorManagerEvent.State) {
|
||||
val stateEvent: TorManagerEvent.State = event
|
||||
val state = stateEvent.torState
|
||||
torState.progressIndicator = state.bootstrap
|
||||
val liveTorState = TorState()
|
||||
liveTorState.progressIndicator = state.bootstrap
|
||||
|
||||
if (state.isOn()) {
|
||||
if (state.bootstrap >= 100) {
|
||||
torState.state = EnumTorState.ON
|
||||
liveTorState.state = EnumTorState.ON
|
||||
} else {
|
||||
torState.state = EnumTorState.STARTING
|
||||
liveTorState.state = EnumTorState.STARTING
|
||||
}
|
||||
} else if (state.isStarting()) {
|
||||
torState.state = EnumTorState.STARTING
|
||||
liveTorState.state = EnumTorState.STARTING
|
||||
} else if (state.isOff()) {
|
||||
torState.state = EnumTorState.OFF
|
||||
liveTorState.state = EnumTorState.OFF
|
||||
} else if (state.isStopping()) {
|
||||
torState.state = EnumTorState.STOPPING
|
||||
liveTorState.state = EnumTorState.STOPPING
|
||||
}
|
||||
torStateLiveData.postValue(liveTorState)
|
||||
}
|
||||
addLine(event.toString())
|
||||
super.onEvent(event)
|
||||
}
|
||||
|
||||
override fun onEvent(event: TorEvent.Type.SingleLineEvent, output: String) {
|
||||
addLine("$event - $output")
|
||||
|
||||
super.onEvent(event, output)
|
||||
}
|
||||
|
||||
override fun onEvent(event: TorEvent.Type.MultiLineEvent, output: List<String>) {
|
||||
addLine("multi-line event: $event. See Logs.")
|
||||
|
||||
// these events are many many many lines and should be moved
|
||||
// off the main thread if ever needed to be dealt with.
|
||||
val enabled = false
|
||||
if (enabled) {
|
||||
appScope.launch(Dispatchers.IO) {
|
||||
Log.d(TAG, "-------------- multi-line event START: $event --------------")
|
||||
for (line in output) {
|
||||
Log.d(TAG, line)
|
||||
}
|
||||
Log.d(TAG, "--------------- multi-line event END: $event ---------------")
|
||||
}
|
||||
}
|
||||
|
||||
super.onEvent(event, output)
|
||||
}
|
||||
|
||||
override fun managerEventError(t: Throwable) {
|
||||
t.printStackTrace()
|
||||
}
|
||||
|
||||
override fun managerEventAddressInfo(info: TorManagerEvent.AddressInfo) {
|
||||
if (info.isNull) {
|
||||
// Tear down HttpClient
|
||||
} else {
|
||||
info.socksInfoToProxyAddressOrNull()?.firstOrNull()?.let { proxyAddress ->
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val socket = InetSocketAddress(proxyAddress.address.value, proxyAddress.port.value)
|
||||
proxy = Proxy(Proxy.Type.SOCKS, socket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun managerEventStartUpCompleteForTorInstance() {
|
||||
// Do one-time things after we're bootstrapped
|
||||
|
||||
appScope.launch {
|
||||
torControlManager.onionAddNew(
|
||||
type = OnionAddress.PrivateKey.Type.ED25519_V3,
|
||||
hsPorts = setOf(HiddenService.Ports(virtualPort = Port(443))),
|
||||
flags = null,
|
||||
maxStreams = null,
|
||||
).onSuccess { hsEntry ->
|
||||
addLine(
|
||||
"New HiddenService: " +
|
||||
"\n - Address: https://${hsEntry.address.canonicalHostname()}" +
|
||||
"\n - PrivateKey: ${hsEntry.privateKey}"
|
||||
)
|
||||
|
||||
torControlManager.onionDel(hsEntry.address).onSuccess {
|
||||
addLine("Aaaaaaaaand it's gone...")
|
||||
}.onFailure { t ->
|
||||
t.printStackTrace()
|
||||
}
|
||||
}.onFailure { t ->
|
||||
t.printStackTrace()
|
||||
}
|
||||
|
||||
delay(20_000L)
|
||||
|
||||
torControlManager.infoGet(TorControlInfoGet.KeyWord.Uptime()).onSuccess { uptime ->
|
||||
addLine("Uptime - $uptime")
|
||||
}.onFailure { t ->
|
||||
t.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object TorKmpManager {
|
||||
private lateinit var torKmp: TorKmp
|
||||
|
||||
@Throws(UninitializedPropertyAccessException::class)
|
||||
fun getTorKmpObject(): TorKmp {
|
||||
return torKmp
|
||||
}
|
||||
|
||||
fun updateTorKmpObject(newKmpObject: TorKmp) {
|
||||
torKmp = newKmpObject
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.robosats.tor
|
||||
|
||||
class TorState {
|
||||
var state : EnumTorState = EnumTorState.OFF
|
||||
get() = field
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
var progressIndicator : Int = 0
|
||||
get() = field
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
}
|
170
mobile_new/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
21
mobile_new/app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
BIN
mobile_new/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
mobile_new/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
mobile_new/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 982 B |
BIN
mobile_new/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
mobile_new/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
mobile_new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
mobile_new/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
mobile_new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
mobile_new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 7.6 KiB |
7
mobile_new/app/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.Robosats" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your dark theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||
</style>
|
||||
</resources>
|
5
mobile_new/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
3
mobile_new/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Robosats</string>
|
||||
</resources>
|
9
mobile_new/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.Robosats" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
</style>
|
||||
|
||||
<style name="Theme.Robosats" parent="Base.Theme.Robosats" />
|
||||
</resources>
|
13
mobile_new/app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
19
mobile_new/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
@ -0,0 +1,17 @@
|
||||
package com.koalasat.robosats
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
5
mobile_new/build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
}
|
23
mobile_new/gradle.properties
Normal file
@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
29
mobile_new/gradle/libs.versions.toml
Normal file
@ -0,0 +1,29 @@
|
||||
[versions]
|
||||
agp = "8.11.1"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.16.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.12.0"
|
||||
activity = "1.10.1"
|
||||
constraintlayout = "2.2.1"
|
||||
kmpTor= "4.8.10-0-1.4.5"
|
||||
kmpTorBinary= "4.8.10-0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
kmp-tor = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor", version.ref = "kmpTor" }
|
||||
kmp-tor-binary = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor-binary-extract-android", version.ref = "kmpTorBinary" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
BIN
mobile_new/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
mobile_new/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Sun Jul 13 16:11:33 CEST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
185
mobile_new/gradlew
vendored
Executable file
@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
89
mobile_new/gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
20
mobile_new/settings.gradle.kts
Normal file
@ -0,0 +1,20 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google ()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
jcenter()
|
||||
maven("https://mvnrepository.com")
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven("https://mvnrepository.com")
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Robosats"
|
||||
include(":app")
|