diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 320be209..434f99fc 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -17,10 +17,10 @@ on: required: true push: branches: [ "main" ] - paths: [ "mobile", "frontend" ] + paths: [ "android", "frontend" ] pull_request: branches: [ "main" ] - paths: [ "mobile", "frontend" ] + paths: [ "android", "frontend" ] jobs: build-android: diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 647e650d..792a9dda 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,7 @@ import com.android.build.api.dsl.Packaging +val baseVersionCode = 81 + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -13,7 +15,7 @@ android { applicationId = "com.robosats" minSdk = 26 targetSdk = 36 - versionCode = 15 + versionCode = baseVersionCode versionName = "0.8.1-alpha" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -60,11 +62,30 @@ android { } } + packaging { jniLibs.useLegacyPackaging = true } } +// Configure unique version codes for ABI splits to prevent downgrade issues +androidComponents { + onVariants { variant -> + val abiCodes = mapOf( + "armeabi-v7a" to 1, + "arm64-v8a" to 2, + "x86" to 3, + "x86_64" to 4 + ) + + variant.outputs.forEach { output -> + val abiName = output.filters.find { it.filterType.name == "ABI" }?.identifier + val abiVersionCode = abiCodes[abiName] ?: 9 // Universal APK gets 9 + output.versionCode.set(baseVersionCode * 1000 + abiVersionCode) + } + } +} + dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) diff --git a/android/app/src/main/java/com/robosats/WebAppInterface.kt b/android/app/src/main/java/com/robosats/WebAppInterface.kt index 23017bae..08d52b63 100644 --- a/android/app/src/main/java/com/robosats/WebAppInterface.kt +++ b/android/app/src/main/java/com/robosats/WebAppInterface.kt @@ -41,8 +41,7 @@ class WebAppInterface(private val context: MainActivity, private val webView: We // Security patterns for input validation private val UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE) - private val SAFE_STRING_PATTERN = Pattern.compile("^[a-zA-Z0-9\s_\-.,:;!?()\[\]{}\"]*$") - + private val SAFE_STRING_PATTERN = Pattern.compile("^[a-zA-Z0-9\\s_\\-.,:;!?()\\[\\]{}\\"]*$") // Maximum length for input strings private val MAX_INPUT_LENGTH = 1000 @@ -108,17 +107,31 @@ class WebAppInterface(private val context: MainActivity, private val webView: We @JavascriptInterface fun copyToClipboard(message: String) { + // Validate input + if (!isValidInput(message, 10000)) { // Allow longer text for clipboard + Log.e(TAG, "Invalid input for copyToClipboard") + Toast.makeText(context, "Invalid content for clipboard", Toast.LENGTH_SHORT).show() + return + } + try { + // Limit clipboard content size for security + val truncatedMessage = if (message.length > 10000) { + message.substring(0, 10000) + "... (content truncated for security)" + } else { + message + } + // Copy to clipboard val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - val clip = android.content.ClipData.newPlainText("RoboSats Data", message) + val clip = android.content.ClipData.newPlainText("RoboSats Data", truncatedMessage) clipboard.setPrimaryClip(clip) // Show a toast notification Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() // Log the action (don't log the content for privacy) - Log.d(TAG, "Text copied to clipboard") + Log.d(TAG, "Text copied to clipboard (${truncatedMessage.length} chars)") } catch (e: Exception) { Log.e(TAG, "Error copying to clipboard", e) Toast.makeText(context, "Failed to copy to clipboard", Toast.LENGTH_SHORT).show() @@ -383,29 +396,6 @@ class WebAppInterface(private val context: MainActivity, private val webView: We resolvePromise(uuid, key) } - @JavascriptInterface - fun restart() { - try { - Log.d(TAG, "Restarting app...") - - val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) - intent?.let { - it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - - context.startActivity(it) - context.finish() - } ?: run { - Log.e(TAG, "Could not get launch intent for app restart") - Toast.makeText(context, "Failed to restart app", Toast.LENGTH_SHORT).show() - } - } catch (e: Exception) { - Log.e(TAG, "Error restarting app", e) - Toast.makeText(context, "Failed to restart app", Toast.LENGTH_SHORT).show() - } - } - private fun onWsMessage(path: String?, message: String?) { val encodedMessage = encodeForJavaScript(message) safeEvaluateJavascript("javascript:window.AndroidRobosats.onWSMessage('$path', '$encodedMessage')")