diff --git a/firebase-auth/core/src/main/AndroidManifest.xml b/firebase-auth/core/src/main/AndroidManifest.xml index b684a0c298..89a122f6e8 100644 --- a/firebase-auth/core/src/main/AndroidManifest.xml +++ b/firebase-auth/core/src/main/AndroidManifest.xml @@ -20,5 +20,9 @@ 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..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 - ReCaptchaOverlay.isSupported(context) -> ReCaptchaOverlay.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/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/ReCaptchaOverlayService.kt similarity index 50% 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/ReCaptchaOverlayService.kt index c7c2cda986..b04ac1c351 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/ReCaptchaOverlayService.kt @@ -7,8 +7,15 @@ 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.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 import android.util.Log @@ -20,19 +27,39 @@ import android.widget.FrameLayout 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 ReCaptchaOverlayService : Service() { + + private var receiver: ResultReceiver? = null + private var hostname: String? = null + private var apiKey: String? = null + + 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) + } - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - var finished = false - var container: View? = null + 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() { @@ -53,11 +80,11 @@ 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) { - cancel() + finishResult(Activity.RESULT_CANCELED) return true } } @@ -65,15 +92,15 @@ 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() + finishResult(Activity.RESULT_CANCELED) return@setOnTouchListener true } val view = container.findViewById(R.id.web) @@ -84,44 +111,63 @@ 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) + windowManager?.addView(container, params) } } - fun cancel() { + fun finishResult(resultCode: Int, token: String? = null) { if (!finished) { finished = true - continuation.resumeWithException(RuntimeException("User cancelled")) + receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } }) } - close() - } - - fun close() { - container?.let { windowManager.removeView(it) } + container?.let { windowManager?.removeView(it) } } companion object { - class ReCaptchaCallback(val overlay: ReCaptchaOverlay) { + + private val recaptchaServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.d(TAG, "onReCaptchaToken: onServiceConnected: $name") + } + + override fun onServiceDisconnected(name: ComponentName?) { + Log.d(TAG, "onReCaptchaToken: onServiceDisconnected: $name") + } + } + + class ReCaptchaCallback(private val overlay: ReCaptchaOverlayService) { @JavascriptInterface fun onReCaptchaToken(token: String) { Log.d(TAG, "onReCaptchaToken: $token") - if (!overlay.finished) { - overlay.finished = true - overlay.continuation.resume(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 -> - ReCaptchaOverlay(context, apiKey, hostname ?: "localhost:5000", continuation).show() + suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine { continuation -> + 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)!!) + } + } catch (e: Exception) { + continuation.resumeWithException(e) + } + } + } + intent.putExtra(EXTRA_API_KEY, apiKey) + intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver) + intent.putExtra(EXTRA_HOSTNAME, hostname) + context.bindService(intent, recaptchaServiceConnection, BIND_AUTO_CREATE) } } } 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 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 @@