Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webview: Fix the exception after multi-process access #2684

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions firebase-auth/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@
android:exported="false"
android:process=":ui"
android:theme="@style/Theme.AppCompat.Light.Dialog.Alert.NoActionBar" />
<service
android:name="org.microg.gms.firebase.auth.ReCaptchaOverlayService"
android:exported="false"
android:process=":ui" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String>) {
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() {
Expand All @@ -53,27 +80,27 @@ 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
}
}
return super.dispatchKeyEvent(event)
}
}

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<WebView>(R.id.web)
Expand All @@ -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<String> { 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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions vending-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@

<activity
android:name="org.microg.vending.billing.ui.PlayWebViewActivity"
android:process=":ui"
android:exported="false" />

<service
Expand Down
Loading