From 0adc120b9d2d2108ee68be2b3321b5f71ea005b1 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Fri, 13 Dec 2024 16:47:10 +0800 Subject: [PATCH 1/3] Webview: Fix the exception after multi-process access --- firebase-auth/core/src/main/AndroidManifest.xml | 4 +++- vending-app/src/main/AndroidManifest.xml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/firebase-auth/core/src/main/AndroidManifest.xml b/firebase-auth/core/src/main/AndroidManifest.xml index b684a0c298..5c7835687f 100644 --- a/firebase-auth/core/src/main/AndroidManifest.xml +++ b/firebase-auth/core/src/main/AndroidManifest.xml @@ -10,7 +10,9 @@ - + diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index eb12ae7378..c22d29a3a0 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -164,6 +164,7 @@ Date: Fri, 13 Dec 2024 21:30:09 +0800 Subject: [PATCH 2/3] Change processing method --- .../core/src/main/AndroidManifest.xml | 9 ++- .../gms/firebase/auth/FirebaseAuthService.kt | 2 +- .../gms/firebase/auth/ReCaptchaActivity.kt | 5 -- ...Overlay.kt => ReCaptchaOverlayActivity.kt} | 77 +++++++++++++++---- .../microg/gms/firebase/auth/extensions.kt | 11 +++ 5 files changed, 79 insertions(+), 25 deletions(-) rename firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/{ReCaptchaOverlay.kt => ReCaptchaOverlayActivity.kt} (56%) create mode 100644 firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/extensions.kt diff --git a/firebase-auth/core/src/main/AndroidManifest.xml b/firebase-auth/core/src/main/AndroidManifest.xml index 5c7835687f..d59d280d84 100644 --- a/firebase-auth/core/src/main/AndroidManifest.xml +++ b/firebase-auth/core/src/main/AndroidManifest.xml @@ -10,9 +10,7 @@ - + @@ -22,5 +20,10 @@ android:exported="false" android:process=":ui" android:theme="@style/Theme.AppCompat.Light.Dialog.Alert.NoActionBar" /> + diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt index 72b599f983..5a09d20a77 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt @@ -371,7 +371,7 @@ class FirebaseAuthServiceImpl(private val context: Context, override val lifecyc Log.d(TAG, "sendVerificationCode") val reCaptchaToken = when { request.request.recaptchaToken != null -> request.request.recaptchaToken - ReCaptchaOverlay.isSupported(context) -> ReCaptchaOverlay.awaitToken(context, apiKey, getAuthorizedDomain()) + ReCaptchaOverlayActivity.isSupported(context) -> ReCaptchaOverlayActivity.awaitToken(context, apiKey, getAuthorizedDomain()) ReCaptchaActivity.isSupported(context) -> ReCaptchaActivity.awaitToken(context, apiKey, getAuthorizedDomain()) else -> throw RuntimeException("No recaptcha token available") } diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaActivity.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaActivity.kt index d52b136541..4311eb3e38 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaActivity.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaActivity.kt @@ -69,11 +69,6 @@ class ReCaptchaActivity : AppCompatActivity() { } companion object { - const val EXTRA_TOKEN = "token" - const val EXTRA_API_KEY = "api_key" - const val EXTRA_HOSTNAME = "hostname" - const val EXTRA_RESULT_RECEIVER = "receiver" - class ReCaptchaCallback(val activity: ReCaptchaActivity) { @JavascriptInterface fun onReCaptchaToken(token: String) { diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlay.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayActivity.kt similarity index 56% rename from firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlay.kt rename to firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayActivity.kt index c7c2cda986..adef958e44 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlay.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayActivity.kt @@ -8,7 +8,13 @@ package org.microg.gms.firebase.auth import android.annotation.SuppressLint import android.app.Activity import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT import android.graphics.PixelFormat +import android.os.Bundle +import android.os.ResultReceiver import android.provider.Settings import android.util.DisplayMetrics import android.util.Log @@ -17,25 +23,34 @@ import android.webkit.JavascriptInterface import android.webkit.WebSettings import android.webkit.WebView import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity import org.microg.gms.firebase.auth.core.R import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager -import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine - private const val TAG = "GmsFirebaseAuthCaptcha" -class ReCaptchaOverlay(val context: Context, val apiKey: String, val hostname: String?, val continuation: Continuation) { +class ReCaptchaOverlayActivity : AppCompatActivity() { + + private val receiver: ResultReceiver? + get() = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER) + private val hostname: String + get() = intent.getStringExtra(EXTRA_HOSTNAME) ?: "localhost:5000" + private var finished = false + private var container: View? = null - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - var finished = false - var container: View? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + show() + } @SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface") private fun show() { + val apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return cancel() + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager val layoutParamsType = if (android.os.Build.VERSION.SDK_INT >= 26) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { @@ -53,7 +68,7 @@ class ReCaptchaOverlay(val context: Context, val apiKey: String, val hostname: S params.x = 0 params.y = 0 - val interceptorLayout: FrameLayout = object : FrameLayout(context) { + val interceptorLayout: FrameLayout = object : FrameLayout(this) { override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (event.action == KeyEvent.ACTION_DOWN) { if (event.keyCode == KeyEvent.KEYCODE_BACK || event.keyCode == KeyEvent.KEYCODE_HOME) { @@ -65,12 +80,12 @@ class ReCaptchaOverlay(val context: Context, val apiKey: String, val hostname: S } } - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater? + val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater? if (inflater != null) { val container = inflater.inflate(R.layout.activity_recaptcha, interceptorLayout) this.container = container container.setBackgroundResource(androidx.appcompat.R.drawable.abc_dialog_material_background) - val pad = (5.0 * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() + val pad = (5.0 * (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() container.setOnTouchListener { v, _ -> v.performClick() cancel() @@ -84,10 +99,10 @@ class ReCaptchaOverlay(val context: Context, val apiKey: String, val hostname: S settings.setSupportZoom(false) settings.displayZoomControls = false settings.cacheMode = WebSettings.LOAD_NO_CACHE - ProfileManager.ensureInitialized(context) + ProfileManager.ensureInitialized(this) settings.userAgentString = Build.generateWebViewUserAgentString(settings.userAgentString) view.addJavascriptInterface(ReCaptchaCallback(this), "MyCallback") - val captcha = context.assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey) + val captcha = assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey) view.loadDataWithBaseURL("https://$hostname/", captcha, null, null, "https://$hostname/") windowManager.addView(container, params) } @@ -96,7 +111,7 @@ class ReCaptchaOverlay(val context: Context, val apiKey: String, val hostname: S fun cancel() { if (!finished) { finished = true - continuation.resumeWithException(RuntimeException("User cancelled")) + finishResult(Activity.RESULT_CANCELED) } close() } @@ -105,14 +120,26 @@ class ReCaptchaOverlay(val context: Context, val apiKey: String, val hostname: S container?.let { windowManager.removeView(it) } } + fun finishResult(resultCode: Int, token: String? = null) { + finished = true + setResult(resultCode, token?.let { Intent().apply { putExtra(EXTRA_TOKEN, it) } }) + receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } }) + finish() + } + + override fun onDestroy() { + super.onDestroy() + cancel() + } + companion object { - class ReCaptchaCallback(val overlay: ReCaptchaOverlay) { + class ReCaptchaCallback(private val overlay: ReCaptchaOverlayActivity) { @JavascriptInterface fun onReCaptchaToken(token: String) { Log.d(TAG, "onReCaptchaToken: $token") if (!overlay.finished) { overlay.finished = true - overlay.continuation.resume(token) + overlay.finishResult(Activity.RESULT_OK, token) } overlay.close() } @@ -120,8 +147,26 @@ class ReCaptchaOverlay(val context: Context, val apiKey: String, val hostname: S fun isSupported(context: Context): Boolean = android.os.Build.VERSION.SDK_INT < 23 || Settings.canDrawOverlays(context) - suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine { continuation -> - ReCaptchaOverlay(context, apiKey, hostname ?: "localhost:5000", continuation).show() + suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine { continuation -> + val intent = Intent(context, ReCaptchaOverlayActivity::class.java) + val resultReceiver = object : ResultReceiver(null) { + override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { + try { + if (resultCode == Activity.RESULT_OK) { + continuation.resume(resultData?.getString(EXTRA_TOKEN)!!) + } + } catch (e: Exception) { + continuation.resumeWithException(e) + } + } + } + intent.putExtra(EXTRA_API_KEY, apiKey) + intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver) + intent.putExtra(EXTRA_HOSTNAME, hostname) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT) + intent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + context.startActivity(intent) } } } diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/extensions.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/extensions.kt new file mode 100644 index 0000000000..66fd1b1f7a --- /dev/null +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/extensions.kt @@ -0,0 +1,11 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.firebase.auth + +const val EXTRA_TOKEN = "token" +const val EXTRA_API_KEY = "api_key" +const val EXTRA_HOSTNAME = "hostname" +const val EXTRA_RESULT_RECEIVER = "receiver" \ No newline at end of file From 768526c90baad208e80c4eb4f65fdbaea2a20250 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Fri, 13 Dec 2024 22:22:53 +0800 Subject: [PATCH 3/3] change to service --- .../core/src/main/AndroidManifest.xml | 7 +- .../gms/firebase/auth/FirebaseAuthService.kt | 2 +- ...Activity.kt => ReCaptchaOverlayService.kt} | 91 ++++++++++--------- 3 files changed, 50 insertions(+), 50 deletions(-) rename firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/{ReCaptchaOverlayActivity.kt => ReCaptchaOverlayService.kt} (70%) diff --git a/firebase-auth/core/src/main/AndroidManifest.xml b/firebase-auth/core/src/main/AndroidManifest.xml index d59d280d84..89a122f6e8 100644 --- a/firebase-auth/core/src/main/AndroidManifest.xml +++ b/firebase-auth/core/src/main/AndroidManifest.xml @@ -20,10 +20,9 @@ android:exported="false" android:process=":ui" android:theme="@style/Theme.AppCompat.Light.Dialog.Alert.NoActionBar" /> - + android:process=":ui" /> diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt index 5a09d20a77..afcb0d4df2 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt @@ -371,7 +371,7 @@ class FirebaseAuthServiceImpl(private val context: Context, override val lifecyc Log.d(TAG, "sendVerificationCode") val reCaptchaToken = when { request.request.recaptchaToken != null -> request.request.recaptchaToken - ReCaptchaOverlayActivity.isSupported(context) -> ReCaptchaOverlayActivity.awaitToken(context, apiKey, getAuthorizedDomain()) + ReCaptchaOverlayService.isSupported(context) -> ReCaptchaOverlayService.awaitToken(context, apiKey, getAuthorizedDomain()) ReCaptchaActivity.isSupported(context) -> ReCaptchaActivity.awaitToken(context, apiKey, getAuthorizedDomain()) else -> throw RuntimeException("No recaptcha token available") } diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayActivity.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayService.kt similarity index 70% rename from firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayActivity.kt rename to firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayService.kt index adef958e44..b04ac1c351 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayActivity.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/ReCaptchaOverlayService.kt @@ -7,13 +7,14 @@ package org.microg.gms.firebase.auth import android.annotation.SuppressLint import android.app.Activity +import android.app.Service +import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT +import android.content.ServiceConnection import android.graphics.PixelFormat import android.os.Bundle +import android.os.IBinder import android.os.ResultReceiver import android.provider.Settings import android.util.DisplayMetrics @@ -23,7 +24,6 @@ import android.webkit.JavascriptInterface import android.webkit.WebSettings import android.webkit.WebView import android.widget.FrameLayout -import androidx.appcompat.app.AppCompatActivity import org.microg.gms.firebase.auth.core.R import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager @@ -33,24 +33,36 @@ import kotlin.coroutines.suspendCoroutine private const val TAG = "GmsFirebaseAuthCaptcha" -class ReCaptchaOverlayActivity : AppCompatActivity() { +class ReCaptchaOverlayService : Service() { + + private var receiver: ResultReceiver? = null + private var hostname: String? = null + private var apiKey: String? = null - private val receiver: ResultReceiver? - get() = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER) - private val hostname: String - get() = intent.getStringExtra(EXTRA_HOSTNAME) ?: "localhost:5000" private var finished = false private var container: View? = null + private var windowManager: WindowManager? = null + + override fun onBind(intent: Intent): IBinder? { + init(intent) + return null + } + + override fun onUnbind(intent: Intent?): Boolean { + finishResult(Activity.RESULT_CANCELED) + return super.onUnbind(intent) + } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + private fun init(intent: Intent) { + apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return finishResult(Activity.RESULT_CANCELED) + receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER) + hostname = intent.getStringExtra(EXTRA_HOSTNAME) ?: "localhost:5000" + windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager show() } @SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface") private fun show() { - val apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return cancel() - val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager val layoutParamsType = if (android.os.Build.VERSION.SDK_INT >= 26) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { @@ -72,7 +84,7 @@ class ReCaptchaOverlayActivity : AppCompatActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (event.action == KeyEvent.ACTION_DOWN) { if (event.keyCode == KeyEvent.KEYCODE_BACK || event.keyCode == KeyEvent.KEYCODE_HOME) { - cancel() + finishResult(Activity.RESULT_CANCELED) return true } } @@ -88,7 +100,7 @@ class ReCaptchaOverlayActivity : AppCompatActivity() { val pad = (5.0 * (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() container.setOnTouchListener { v, _ -> v.performClick() - cancel() + finishResult(Activity.RESULT_CANCELED) return@setOnTouchListener true } val view = container.findViewById(R.id.web) @@ -102,55 +114,47 @@ class ReCaptchaOverlayActivity : AppCompatActivity() { ProfileManager.ensureInitialized(this) settings.userAgentString = Build.generateWebViewUserAgentString(settings.userAgentString) view.addJavascriptInterface(ReCaptchaCallback(this), "MyCallback") - val captcha = assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey) + val captcha = assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey!!) view.loadDataWithBaseURL("https://$hostname/", captcha, null, null, "https://$hostname/") - windowManager.addView(container, params) + windowManager?.addView(container, params) } } - fun cancel() { + fun finishResult(resultCode: Int, token: String? = null) { if (!finished) { finished = true - finishResult(Activity.RESULT_CANCELED) + receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } }) } - close() + container?.let { windowManager?.removeView(it) } } - fun close() { - container?.let { windowManager.removeView(it) } - } + companion object { - fun finishResult(resultCode: Int, token: String? = null) { - finished = true - setResult(resultCode, token?.let { Intent().apply { putExtra(EXTRA_TOKEN, it) } }) - receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } }) - finish() - } + private val recaptchaServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.d(TAG, "onReCaptchaToken: onServiceConnected: $name") + } - override fun onDestroy() { - super.onDestroy() - cancel() - } + override fun onServiceDisconnected(name: ComponentName?) { + Log.d(TAG, "onReCaptchaToken: onServiceDisconnected: $name") + } + } - companion object { - class ReCaptchaCallback(private val overlay: ReCaptchaOverlayActivity) { + class ReCaptchaCallback(private val overlay: ReCaptchaOverlayService) { @JavascriptInterface fun onReCaptchaToken(token: String) { Log.d(TAG, "onReCaptchaToken: $token") - if (!overlay.finished) { - overlay.finished = true - overlay.finishResult(Activity.RESULT_OK, token) - } - overlay.close() + overlay.finishResult(Activity.RESULT_OK, token) } } fun isSupported(context: Context): Boolean = android.os.Build.VERSION.SDK_INT < 23 || Settings.canDrawOverlays(context) suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine { continuation -> - val intent = Intent(context, ReCaptchaOverlayActivity::class.java) + val intent = Intent(context, ReCaptchaOverlayService::class.java) val resultReceiver = object : ResultReceiver(null) { override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { + context.unbindService(recaptchaServiceConnection) try { if (resultCode == Activity.RESULT_OK) { continuation.resume(resultData?.getString(EXTRA_TOKEN)!!) @@ -163,10 +167,7 @@ class ReCaptchaOverlayActivity : AppCompatActivity() { intent.putExtra(EXTRA_API_KEY, apiKey) intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver) intent.putExtra(EXTRA_HOSTNAME, hostname) - intent.addFlags(FLAG_ACTIVITY_NEW_TASK) - intent.addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT) - intent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - context.startActivity(intent) + context.bindService(intent, recaptchaServiceConnection, BIND_AUTO_CREATE) } } }