This commit is contained in:
koalasat
2025-07-16 12:36:32 +02:00
parent 05c7d02c4f
commit 431aa512d7
13 changed files with 80 additions and 50 deletions

View File

@ -8,6 +8,7 @@ declare global {
interface AndroidAppRobosats { interface AndroidAppRobosats {
generateRoboname: (uuid: string, initialString: string) => void; generateRoboname: (uuid: string, initialString: string) => void;
generateRobohash: (uuid: string, initialString: string) => void;
} }
class AndroidRobosats { class AndroidRobosats {

View File

@ -32,15 +32,20 @@ class RoboidentitiesAndroidClient implements RoboidentitiesClient {
if (this.robohashes[key]) { if (this.robohashes[key]) {
return this.robohashes[key]; return this.robohashes[key];
} else { } else {
const response = await window.NativeRobosats?.postMessage({ try {
category: 'roboidentities', const result = await new Promise<string>((resolve, reject) => {
type: 'robohash', const uuid: string = uuidv4();
detail: key, window.AndroidAppRobosats?.generateRobohash(uuid, initialString);
}); window.AndroidRobosats?.storePromise(uuid, resolve, reject);
const result: string = response ? Object.values(response)[0] : ''; });
const image: string = `data:image/png;base64,${result}`;
this.robohashes[key] = image; const image: string = `data:image/png;base64,${result}`;
return image; this.robohashes[key] = image;
return image;
} catch (error) {
console.error('Error generating robohash:', error);
return '';
}
} }
}; };
} }

View File

@ -6,11 +6,11 @@ plugins {
} }
android { android {
namespace = "com.koalasat.robosats" namespace = "com.robosats"
compileSdk = 36 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "com.koalasat.robosats" applicationId = "com.robosats"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1

View File

@ -1,4 +1,4 @@
package com.koalasat.robosats package com.robosats
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.koalasat.robosats", appContext.packageName) assertEquals("com.robosats", appContext.packageName)
} }
} }

View File

@ -17,7 +17,7 @@
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:theme="@style/Theme.Robosats"> android:theme="@style/Theme.Robosats">
<activity <activity
android:name=".MainActivity" android:name="com.robosats.MainActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -1,15 +1,21 @@
package com.koalasat.robosats package com.robosats
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.webkit.* import android.webkit.*
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.koalasat.robosats.WebAppInterface import com.robosats.R
import com.robosats.tor.TorKmp import com.robosats.tor.TorKmp
import com.robosats.tor.TorKmpManager import com.robosats.tor.TorKmpManager
import java.io.ByteArrayInputStream
import java.net.HttpURLConnection
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView private lateinit var webView: WebView
@ -50,10 +56,10 @@ class MainActivity : AppCompatActivity() {
// Show a toast notification about the critical error // Show a toast notification about the critical error
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Critical error: Tor initialization failed. App cannot proceed securely.", "Critical error: Tor initialization failed. App cannot proceed securely.",
android.widget.Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
} }
@ -66,10 +72,10 @@ class MainActivity : AppCompatActivity() {
try { try {
// Display connecting message // Display connecting message
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Connecting to Tor network...", "Connecting to Tor network...",
android.widget.Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@ -84,10 +90,10 @@ class MainActivity : AppCompatActivity() {
// Update status on UI thread every few retries // Update status on UI thread every few retries
if (retries % 3 == 0) { if (retries % 3 == 0) {
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Still connecting to Tor (attempt $retries/$maxRetries)...", "Still connecting to Tor (attempt $retries/$maxRetries)...",
android.widget.Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} }
@ -99,10 +105,10 @@ class MainActivity : AppCompatActivity() {
// Show success message // Show success message
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Tor connected successfully", "Tor connected successfully",
android.widget.Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
// Now that Tor is connected, set up the WebView // Now that Tor is connected, set up the WebView
@ -113,10 +119,10 @@ class MainActivity : AppCompatActivity() {
Log.e("TorInitialization", "Failed to connect to Tor after $maxRetries retries") Log.e("TorInitialization", "Failed to connect to Tor after $maxRetries retries")
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Failed to connect to Tor after multiple attempts. App cannot proceed securely.", "Failed to connect to Tor after multiple attempts. App cannot proceed securely.",
android.widget.Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
} }
@ -124,10 +130,10 @@ class MainActivity : AppCompatActivity() {
Log.e("TorInitialization", "Error during Tor connection: ${e.message}", e) Log.e("TorInitialization", "Error during Tor connection: ${e.message}", e)
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Error connecting to Tor: ${e.message}", "Error connecting to Tor: ${e.message}",
android.widget.Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
} }
@ -190,10 +196,10 @@ class MainActivity : AppCompatActivity() {
// Show message that we're setting up secure browsing // Show message that we're setting up secure browsing
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Setting up secure Tor browsing...", "Setting up secure Tor browsing...",
android.widget.Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@ -225,10 +231,10 @@ class MainActivity : AppCompatActivity() {
// Success - now configure WebViewClient and load URL on UI thread // Success - now configure WebViewClient and load URL on UI thread
runOnUiThread { runOnUiThread {
android.widget.Toast.makeText( Toast.makeText(
this, this,
"Secure connection established", "Secure connection established",
android.widget.Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
// Create a custom WebViewClient that forces all traffic through Tor // Create a custom WebViewClient that forces all traffic through Tor
@ -249,14 +255,14 @@ class MainActivity : AppCompatActivity() {
// For .onion domains, we must use SOCKS proxy type // For .onion domains, we must use SOCKS proxy type
val proxyType = if (isOnionDomain) val proxyType = if (isOnionDomain)
java.net.Proxy.Type.SOCKS Proxy.Type.SOCKS
else else
java.net.Proxy.Type.HTTP Proxy.Type.HTTP
// Create a proxy instance for Tor with the appropriate type // Create a proxy instance for Tor with the appropriate type
val torProxy = java.net.Proxy( val torProxy = Proxy(
proxyType, proxyType,
java.net.InetSocketAddress(proxyHost, proxyPort) InetSocketAddress(proxyHost, proxyPort)
) )
if (isOnionDomain) { if (isOnionDomain) {
@ -264,14 +270,14 @@ class MainActivity : AppCompatActivity() {
} }
// Create connection with proxy already configured // Create connection with proxy already configured
val url = java.net.URL(urlString) val url = URL(urlString)
val connection = url.openConnection(torProxy) val connection = url.openConnection(torProxy)
// Configure basic connection properties // Configure basic connection properties
connection.connectTimeout = 60000 // Longer timeout for onion domains connection.connectTimeout = 60000 // Longer timeout for onion domains
connection.readTimeout = 60000 connection.readTimeout = 60000
if (connection is java.net.HttpURLConnection) { if (connection is HttpURLConnection) {
// Ensure no connection reuse to prevent proxy leaks // Ensure no connection reuse to prevent proxy leaks
connection.setRequestProperty("Connection", "close") connection.setRequestProperty("Connection", "close")
@ -305,7 +311,7 @@ class MainActivity : AppCompatActivity() {
// Get the correct input stream based on response code // Get the correct input stream based on response code
val inputStream = if (responseCode >= 400) { val inputStream = if (responseCode >= 400) {
connection.errorStream ?: java.io.ByteArrayInputStream(byteArrayOf()) connection.errorStream ?: ByteArrayInputStream(byteArrayOf())
} else { } else {
connection.inputStream connection.inputStream
} }
@ -371,7 +377,7 @@ class MainActivity : AppCompatActivity() {
super.onReceivedError(view, request, error) super.onReceivedError(view, request, error)
} }
override fun onPageStarted(view: WebView, url: String, favicon: android.graphics.Bitmap?) { override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// Verify Tor is connected when page starts loading // Verify Tor is connected when page starts loading
if (!torKmp.isConnected()) { if (!torKmp.isConnected()) {
Log.e("SecurityError", "Tor disconnected as page started loading") Log.e("SecurityError", "Tor disconnected as page started loading")
@ -409,10 +415,10 @@ class MainActivity : AppCompatActivity() {
// Show error and exit - DO NOT LOAD WEBVIEW // Show error and exit - DO NOT LOAD WEBVIEW
runOnUiThread { runOnUiThread {
// Show toast with error // Show toast with error
android.widget.Toast.makeText( Toast.makeText(
this, this,
"SECURITY ERROR: Cannot set up secure browsing: ${e.message}", "SECURITY ERROR: Cannot set up secure browsing: ${e.message}",
android.widget.Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
} }
@ -478,7 +484,7 @@ class MainActivity : AppCompatActivity() {
try { try {
// Try to set global proxy through ConnectivityManager // Try to set global proxy through ConnectivityManager
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) val connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE)
val setDefaultProxyMethod = connectivityManager.javaClass.getDeclaredMethod("setDefaultProxy", proxyClass) val setDefaultProxyMethod = connectivityManager.javaClass.getDeclaredMethod("setDefaultProxy", proxyClass)
setDefaultProxyMethod.isAccessible = true setDefaultProxyMethod.isAccessible = true
setDefaultProxyMethod.invoke(connectivityManager, proxyInfo) setDefaultProxyMethod.invoke(connectivityManager, proxyInfo)

View File

@ -1,4 +1,4 @@
package com.koalasat.robosats package com.robosats
import android.util.Log import android.util.Log

View File

@ -1,4 +1,4 @@
package com.koalasat.robosats package com.robosats
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
@ -26,13 +26,11 @@ class WebAppInterface(private val context: Context, private val webView: WebView
fun generateRoboname(uuid: String, message: String) { fun generateRoboname(uuid: String, message: String) {
try { try {
val roboname = roboIdentities.generateRoboname(message) val roboname = roboIdentities.generateRoboname(message)
Log.d(TAG, "Generated roboname: $roboname for message: $message")
webView.post { webView.post {
webView.evaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('${uuid}', '${roboname}')", null) webView.evaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('${uuid}', '${roboname}')", null)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error in generateRoboname: ${e.message}", e) Log.e(TAG, "Error in generateRoboname", e)
// Handle error gracefully by returning a fallback value // Handle error gracefully by returning a fallback value
webView.post { webView.post {
@ -43,4 +41,24 @@ class WebAppInterface(private val context: Context, private val webView: WebView
} }
} }
} }
@JavascriptInterface
fun generateRobohash(uuid: String, message: String) {
try {
val roboname = roboIdentities.generateRobohash(message)
webView.post {
webView.evaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('${uuid}', '${roboname}')", null)
}
} catch (e: Exception) {
Log.e(TAG, "Error in generateRobohash", e)
// Handle error gracefully by returning a fallback value
webView.post {
webView.evaluateJavascript(
"javascript:window.AndroidRobosats.onRejectPromise('${uuid}', 'Error generating robot hash')",
null
)
}
}
}
} }

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context=".MainActivity"> tools:context="com.robosats.MainActivity">
<WebView <WebView
android:id="@+id/webView" android:id="@+id/webView"

View File

@ -1,4 +1,4 @@
package com.koalasat.robosats package com.robosats
import org.junit.Test import org.junit.Test