diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml
index 4d8ffaddee..6261d9580f 100644
--- a/play-services-core/src/main/AndroidManifest.xml
+++ b/play-services-core/src/main/AndroidManifest.xml
@@ -170,6 +170,11 @@
android:name="android.permission.UPDATE_APP_OPS_STATS"
tools:ignore="ProtectedPermissions" />
+
+
+
+
+
+
diff --git a/play-services-core/src/main/java/org/microg/gms/settings/GmsFileProvider.kt b/play-services-core/src/main/java/org/microg/gms/settings/GmsFileProvider.kt
new file mode 100644
index 0000000000..39f2b2374c
--- /dev/null
+++ b/play-services-core/src/main/java/org/microg/gms/settings/GmsFileProvider.kt
@@ -0,0 +1,69 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.settings
+
+import android.content.Context
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.os.CancellationSignal
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import androidx.core.content.FileProvider
+import java.io.FileNotFoundException
+
+private const val TAG = "GmsFileProvider"
+
+class GmsFileProvider : FileProvider() {
+ private val emptyProjection = arrayOfNulls(0)
+ private var initializationFailed = false
+
+ override fun attachInfo(context: Context, info: ProviderInfo) {
+ try {
+ super.attachInfo(context, info)
+ } catch (e: Exception) {
+ initializationFailed = true
+ Log.e(TAG, "attachInfo error:${e.message}")
+ }
+ }
+
+ override fun onCreate(): Boolean {
+ return true
+ }
+
+ override fun getType(uri: Uri): String? {
+ if (initializationFailed) {
+ return null
+ }
+ return super.getType(uri)
+ }
+
+ override fun openFile(
+ uri: Uri, mode: String, signal: CancellationSignal?
+ ): ParcelFileDescriptor? {
+ if (!initializationFailed) {
+ return super.openFile(uri, mode, signal)
+ }
+ throw FileNotFoundException("FileProvider creation failed")
+ }
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
+ if (initializationFailed) {
+ return 0
+ }
+ return super.delete(uri, selection, selectionArgs)
+ }
+
+ override fun query(
+ uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?
+ ): Cursor {
+ if (initializationFailed) {
+ return MatrixCursor(emptyProjection)
+ }
+ return super.query(uri, projection, selection, selectionArgs, sortOrder)
+ }
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt
index a08ac95637..c1b6108438 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt
@@ -7,26 +7,49 @@ package org.microg.gms.accountsettings.ui
import android.accounts.Account
import android.accounts.AccountManager
-import android.content.Intent
+import android.graphics.Color
+import android.graphics.Typeface
+import android.net.Uri
+import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
+import android.view.Gravity
import android.view.View
-import android.webkit.JavascriptInterface
+import android.webkit.ValueCallback
import android.webkit.WebView
import android.widget.ProgressBar
import android.widget.RelativeLayout
import android.widget.RelativeLayout.LayoutParams.MATCH_PARENT
import android.widget.RelativeLayout.LayoutParams.WRAP_CONTENT
+import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.json.JSONException
-import org.json.JSONObject
+import androidx.appcompat.widget.Toolbar
+import androidx.core.content.ContextCompat
+import com.google.android.gms.R
+import org.microg.gms.accountsettings.ui.bridge.OcAdvertisingIdBridge
+import org.microg.gms.accountsettings.ui.bridge.OcAndroidIdBridge
+import org.microg.gms.accountsettings.ui.bridge.OcAppBarBridge
+import org.microg.gms.accountsettings.ui.bridge.OcAppPermissionsBridge
+import org.microg.gms.accountsettings.ui.bridge.OcClientInfoBridge
+import org.microg.gms.accountsettings.ui.bridge.OcConsistencyBridge
+import org.microg.gms.accountsettings.ui.bridge.OcContactsBridge
+import org.microg.gms.accountsettings.ui.bridge.OcFido2Bridge
+import org.microg.gms.accountsettings.ui.bridge.OcFidoU2fBridge
+import org.microg.gms.accountsettings.ui.bridge.OcFilePickerBridge
+import org.microg.gms.accountsettings.ui.bridge.OcFolsomBridge
+import org.microg.gms.accountsettings.ui.bridge.OcPermissionsBridge
+import org.microg.gms.accountsettings.ui.bridge.OcPlayProtectBridge
+import org.microg.gms.accountsettings.ui.bridge.OcTelephonyBridge
+import org.microg.gms.accountsettings.ui.bridge.OcTrustAgentBridge
+import org.microg.gms.accountsettings.ui.bridge.OcUdcBridge
+import org.microg.gms.accountsettings.ui.bridge.OcUiBridge
import org.microg.gms.auth.AuthConstants
import org.microg.gms.common.Constants
import org.microg.gms.people.PeopleManager
+import org.microg.gms.profile.ProfileManager
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
private const val TAG = "AccountSettings"
@@ -62,7 +85,7 @@ private val SCREEN_ID_TO_URL = hashMapOf(
310 to "https://myaccount.google.com/reservations",
312 to "https://myaccount.google.com/accessibility",
313 to "https://myaccount.google.com/inputtools",
- 400 to "https://myaccount.google.com/security-checkup/",
+ 400 to "https://myaccount.google.com/security-checkup",
401 to "https://myaccount.google.com/signinoptions/password",
403 to "https://myaccount.google.com/signinoptions/two-step-verification",
406 to "https://myaccount.google.com/signinoptions/rescuephone",
@@ -94,7 +117,7 @@ private val SCREEN_ID_TO_URL = hashMapOf(
10007 to "https://myaccount.google.com/payments-and-subscriptions",
10015 to "https://support.google.com/accounts",
10050 to "https://myaccount.google.com/profile",
- 10052 to "https://myaccount.google.com/embedded/family/create",
+ 10052 to "https://myaccount.google.com/family/details",
10090 to "https://myaccount.google.com/profile/name",
10704 to "https://www.google.com/account/about",
10706 to "https://myaccount.google.com/profile/profiles-summary",
@@ -134,11 +157,20 @@ private val ACTION_TO_SCREEN_ID = hashMapOf(
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
- private var accountName: String? = null
- private var resultBundle: Bundle? = null
+ private val executor: ExecutorService = Executors.newSingleThreadExecutor()
private fun getSelectedAccountName(): String? = null
+ private var filePathCallback: ValueCallback>? = null
+ private val pickerUtils = PicturePickerUtils(this, {
+ filePathCallback?.onReceiveValue(if (it != null) arrayOf(it) else emptyArray())
+ filePathCallback = null
+ }, {
+ Log.d(TAG, "Picker error : ${it.name}<${it.value}>")
+ filePathCallback?.onReceiveValue(emptyArray())
+ filePathCallback = null
+ })
+
override fun onCreate(savedInstanceState: Bundle?) {
val extras = intent?.extras?.also { it.keySet() }
Log.d(TAG, "Invoked with ${intent.action} and extras $extras")
@@ -156,7 +188,7 @@ class MainActivity : AppCompatActivity() {
val callingPackage = intent?.getStringExtra(EXTRA_CALLING_PACKAGE_NAME) ?: callingActivity?.packageName ?: Constants.GMS_PACKAGE_NAME
val ignoreAccount = intent?.getBooleanExtra(EXTRA_IGNORE_ACCOUNT, false) ?: false
- accountName = if (ignoreAccount) null else {
+ val accountName = if (ignoreAccount) null else {
val accounts = AccountManager.get(this).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)
val accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME) ?: intent.getParcelableExtra("account")?.name ?: getSelectedAccountName()
accounts.find { it.name.equals(accountName) }?.name
@@ -175,18 +207,45 @@ class MainActivity : AppCompatActivity() {
} else { this }
}
val layout = RelativeLayout(this)
- layout.addView(ProgressBar(this).apply {
+ val titleView = TextView(this).apply {
+ text = ContextCompat.getString(context, R.string.pref_accounts_title)
+ textSize = 20f
+ setTextColor(Color.BLACK)
+ maxLines = 1
+ ellipsize = TextUtils.TruncateAt.END
+ setTypeface(Typeface.create("sans-serif", Typeface.NORMAL))
+ layoutParams = Toolbar.LayoutParams(MATCH_PARENT, WRAP_CONTENT, Gravity.START)
+ }
+ val toolbar = Toolbar(this).apply {
+ id = View.generateViewId()
+ setBackgroundColor(Color.WHITE)
+ if (SDK_INT >= 21) {
+ backgroundTintList = null
+ }
+ layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
+ addRule(RelativeLayout.ALIGN_PARENT_TOP)
+ }
+ navigationIcon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_close)
+ setNavigationOnClickListener { finish() }
+ addView(titleView)
+ }
+ val progressBar = ProgressBar(this).apply {
+ id = View.generateViewId()
layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
- addRule(RelativeLayout.CENTER_HORIZONTAL)
- addRule(RelativeLayout.CENTER_VERTICAL)
+ addRule(RelativeLayout.CENTER_IN_PARENT)
}
isIndeterminate = true
- })
+ }
webView = WebView(this).apply {
- layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ id = View.generateViewId()
+ layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).apply {
+ addRule(RelativeLayout.BELOW, toolbar.id)
+ }
visibility = View.INVISIBLE
- addJavascriptInterface(UiBridge(), "ocUi")
+ loadJsBridge(accountName, toolbar)
}
+ layout.addView(toolbar)
+ layout.addView(progressBar)
layout.addView(webView)
setContentView(layout)
WebViewHelper(this, webView, ALLOWED_WEB_PREFIXES).openWebView(screenUrl, accountName, callingPackage)
@@ -200,6 +259,9 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
+ if (!executor.isShutdown) {
+ executor.shutdown()
+ }
}
override fun onBackPressed() {
@@ -210,125 +272,39 @@ class MainActivity : AppCompatActivity() {
}
}
- private fun updateLocalAccountAvatar(newAvatarUrl: String?) {
- if (TextUtils.isEmpty(newAvatarUrl) || accountName == null) {
- return
- }
- lifecycleScope.launchWhenCreated {
- withContext(Dispatchers.IO) {
- PeopleManager.updateOwnerAvatar(this@MainActivity, accountName, newAvatarUrl)
- }
- }
+ private fun WebView.loadJsBridge(accountName: String?, toolbar: Toolbar) {
+ ProfileManager.ensureInitialized(this@MainActivity)
+ addJavascriptInterface(OcUiBridge(this@MainActivity, accountName, this), OcUiBridge.NAME)
+ addJavascriptInterface(OcConsistencyBridge(), OcConsistencyBridge.NAME)
+ addJavascriptInterface(OcAppBarBridge(toolbar, this), OcAppBarBridge.NAME)
+ addJavascriptInterface(OcPlayProtectBridge(this), OcPlayProtectBridge.NAME)
+ addJavascriptInterface(OcTrustAgentBridge(this), OcTrustAgentBridge.NAME)
+ addJavascriptInterface(OcPermissionsBridge(this), OcPermissionsBridge.NAME)
+ addJavascriptInterface(OcFido2Bridge(this), OcFido2Bridge.NAME)
+ addJavascriptInterface(OcClientInfoBridge(), OcClientInfoBridge.NAME)
+ addJavascriptInterface(OcTelephonyBridge(), OcTelephonyBridge.NAME)
+ addJavascriptInterface(OcUdcBridge(this), OcUdcBridge.NAME)
+ addJavascriptInterface(OcAdvertisingIdBridge(this@MainActivity), OcAdvertisingIdBridge.NAME)
+ addJavascriptInterface(OcAndroidIdBridge(this@MainActivity), OcAndroidIdBridge.NAME)
+ addJavascriptInterface(OcAppPermissionsBridge(this), OcAppPermissionsBridge.NAME)
+ addJavascriptInterface(OcFolsomBridge(), OcFolsomBridge.NAME)
+ addJavascriptInterface(OcFidoU2fBridge(this), OcFidoU2fBridge.NAME)
+ addJavascriptInterface(OcContactsBridge(this), OcContactsBridge.NAME)
+ addJavascriptInterface(OcFilePickerBridge(this@MainActivity, this, executor), OcFilePickerBridge.NAME)
}
- private inner class UiBridge {
-
- @JavascriptInterface
- fun close() {
- Log.d(TAG, "close: ")
- val intent = Intent()
- if (resultBundle != null) {
- intent.putExtras(resultBundle!!)
- }
- setResult(RESULT_OK, intent)
- finish()
- }
-
- @JavascriptInterface
- fun closeWithResult(resultJsonStr: String?) {
- Log.d(TAG, "closeWithResult: resultJsonStr -> $resultJsonStr")
- setResult(resultJsonStr)
- close()
- }
-
- @JavascriptInterface
- fun goBackOrClose() {
- Log.d(TAG, "goBackOrClose: ")
- onBackPressed()
- }
-
- @JavascriptInterface
- fun hideKeyboard() {
- Log.d(TAG, "hideKeyboard: ")
- }
-
- @JavascriptInterface
- fun isCloseWithResultSupported(): Boolean {
- return true
- }
-
- @JavascriptInterface
- fun isOpenHelpEnabled(): Boolean {
- return true
- }
-
- @JavascriptInterface
- fun isOpenScreenEnabled(): Boolean {
- return true
- }
-
- @JavascriptInterface
- fun isSetResultSupported(): Boolean {
- return true
- }
-
- @JavascriptInterface
- fun open(str: String?) {
- Log.d(TAG, "open: str -> $str")
- }
-
- @JavascriptInterface
- fun openHelp(str: String?) {
- Log.d(TAG, "openHelp: str -> $str")
- }
-
- @JavascriptInterface
- fun openScreen(screenId: Int, str: String?) {
- Log.d(TAG, "openScreen: screenId -> $screenId str -> $str accountName -> $accountName")
- val intent = Intent(this@MainActivity, MainActivity::class.java).apply {
- putExtra(EXTRA_SCREEN_ID, screenId)
- putExtra(EXTRA_ACCOUNT_NAME, accountName)
- }
- startActivity(intent)
- }
-
- @JavascriptInterface
- fun setBackStop() {
- Log.d(TAG, "setBackStop: ")
- webView.clearHistory()
- }
+ fun showImageChooser(targetFilePathCallback: ValueCallback>) {
+ filePathCallback?.onReceiveValue(null)
+ filePathCallback = targetFilePathCallback
+ pickerUtils.launchChooser("*/*")
+ }
- @JavascriptInterface
- fun setResult(resultJsonStr: String?) {
- Log.d(TAG, "setResult: resultJsonStr -> $resultJsonStr")
- val map = jsonToMap(resultJsonStr) ?: return
- if (map.containsKey(KEY_UPDATED_PHOTO_URL)) {
- updateLocalAccountAvatar(map[KEY_UPDATED_PHOTO_URL])
- }
- resultBundle = Bundle().apply {
- for ((key, value) in map) {
- putString("result.$key", value)
- }
- }
+ fun updateLocalAccountAvatar(newAvatarUrl: String?, accountName: String?) {
+ if (TextUtils.isEmpty(newAvatarUrl) || accountName == null) {
+ return
}
-
- private fun jsonToMap(jsonStr: String?): Map? {
- val hashMap = HashMap()
- if (!jsonStr.isNullOrEmpty()) {
- try {
- val jSONObject = JSONObject(jsonStr)
- val keys = jSONObject.keys()
- while (keys.hasNext()) {
- val next = keys.next()
- val obj = jSONObject[next]
- hashMap[next] = obj as String
- }
- } catch (e: JSONException) {
- Log.d(TAG, "Unable to parse result JSON string", e)
- return null
- }
- }
- return hashMap
+ executor.submit {
+ PeopleManager.updateOwnerAvatar(this, accountName, newAvatarUrl)
}
}
}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/PicturePickerUtils.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/PicturePickerUtils.kt
new file mode 100644
index 0000000000..268699295e
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/PicturePickerUtils.kt
@@ -0,0 +1,115 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.provider.MediaStore
+import android.util.Log
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import java.io.File
+
+private const val CAMERA_TEMP_DIR = "octa_camera_temp"
+private const val TAG = "PicturePickerUtils"
+
+class PicturePickerUtils(private val activity: MainActivity, private val resultCallback: (Uri?) -> Unit, private val errorCallback: (ResultStatus) -> Unit) {
+ private lateinit var fileChooserLauncher: ActivityResultLauncher
+ private var currentPhotoUri: Uri? = null
+
+ init {
+ initializeChooserLauncher()
+ }
+
+ private fun initializeChooserLauncher() {
+ fileChooserLauncher = activity.registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val uri = result.data?.data
+ if (uri != null) {
+ resultCallback(uri)
+ } else if (currentPhotoUri != null) {
+ resultCallback(currentPhotoUri)
+ } else {
+ errorCallback(ResultStatus.FAILED)
+ }
+ } else {
+ errorCallback(ResultStatus.USER_CANCEL)
+ }
+ }
+ }
+
+ fun launchChooser(mimeType: String) {
+ if (mimeType.startsWith("image/") || mimeType == "image/*" || mimeType == "*/*") {
+ when {
+ ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
+ launchChooserInternal(mimeType)
+ }
+
+ else -> {
+ launchFilePickerOnly(mimeType)
+ }
+ }
+ } else {
+ launchFilePickerOnly(mimeType)
+ }
+ }
+
+ private fun launchFilePickerOnly(mimeType: String) {
+ val getContentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
+ type = mimeType
+ addCategory(Intent.CATEGORY_OPENABLE)
+ }
+ fileChooserLauncher.launch(getContentIntent)
+ }
+
+ private fun launchChooserInternal(mimeType: String) {
+ val getContentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
+ type = mimeType
+ addCategory(Intent.CATEGORY_OPENABLE)
+ }
+ val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ if (takePictureIntent.resolveActivity(activity.packageManager) != null) {
+ currentPhotoUri = createImageUri()
+ if (currentPhotoUri != null) {
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentPhotoUri)
+ val chooserIntent = Intent.createChooser(getContentIntent, "Choose")
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(takePictureIntent))
+ fileChooserLauncher.launch(chooserIntent)
+ } else {
+ fileChooserLauncher.launch(getContentIntent)
+ }
+ } else {
+ fileChooserLauncher.launch(getContentIntent)
+ }
+ }
+
+ private fun createImageUri(): Uri? {
+ try {
+ val cacheDir = activity.cacheDir
+ val cameraDir = File(cacheDir, CAMERA_TEMP_DIR)
+ if (!cameraDir.exists()) {
+ cameraDir.mkdirs()
+ }
+ val photoFile = File(cameraDir, "camera_temp.jpg")
+ if (photoFile.exists()) {
+ photoFile.delete()
+ }
+ photoFile.createNewFile()
+ return FileProvider.getUriForFile(activity, "${activity.packageName}.fileprovider", photoFile)
+ } catch (e: Exception) {
+ Log.w(TAG, "createImageUri: ", e)
+ return null
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt
index 25ddc5e075..f11bc46128 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt
@@ -9,13 +9,16 @@ import android.content.Intent
import android.content.Intent.URI_INTENT_SCHEME
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
+import android.provider.Settings
import android.util.Log
import android.view.View
import android.webkit.CookieManager
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
-import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebResourceErrorCompat
import androidx.webkit.WebViewClientCompat
@@ -23,8 +26,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
+import org.microg.gms.auth.AuthConstants
import org.microg.gms.auth.AuthManager
-import org.microg.gms.common.Constants
+import org.microg.gms.auth.login.LoginActivity
import org.microg.gms.common.Constants.GMS_PACKAGE_NAME
import org.microg.gms.common.PackageUtils
import java.net.URLEncoder
@@ -32,9 +36,20 @@ import java.util.*
private const val TAG = "AccountSettingsWebView"
-class WebViewHelper(private val activity: AppCompatActivity, private val webView: WebView, private val allowedPrefixes: Set = emptySet()) {
+class WebViewHelper(private val activity: MainActivity, private val webView: WebView, private val allowedPrefixes: Set = emptySet()) {
+ private var saveUserAvatar = false
fun openWebView(url: String?, accountName: String?, callingPackage: String? = null) {
- prepareWebViewSettings(webView.settings)
+ prepareWebViewSettings(webView.settings, callingPackage)
+ webView.webChromeClient = object : WebChromeClient() {
+ override fun onShowFileChooser(
+ view: WebView?,
+ filePathCallback: ValueCallback>,
+ fileChooserParams: FileChooserParams
+ ): Boolean {
+ activity.showImageChooser(filePathCallback)
+ return true
+ }
+ }
webView.webViewClient = object : WebViewClientCompat() {
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceErrorCompat) {
Log.w(TAG, "Error loading: $error")
@@ -45,6 +60,26 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView
webView.visibility = View.VISIBLE
}
+ override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
+ if (SDK_INT >= 21) {
+ val requestUrl = request?.url?.toString() ?: return super.shouldInterceptRequest(view, request)
+ Log.d(TAG, "shouldInterceptRequest to $requestUrl")
+ try {
+ if (saveUserAvatar && isGoogleAvatarUrl(requestUrl)) {
+ activity.updateLocalAccountAvatar(requestUrl, accountName)
+ saveUserAvatar = false
+ }
+ val overrideUri = Uri.parse(requestUrl)
+ if (overrideUri.getQueryParameter("source-path") == "/profile-picture/updating") {
+ saveUserAvatar = true
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "shouldInterceptRequest: error", e)
+ }
+ }
+ return super.shouldInterceptRequest(view, request)
+ }
+
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
Log.d(TAG, "Navigating to $url")
if (url.startsWith("intent:")) {
@@ -53,6 +88,7 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView
if (intent.`package` == GMS_PACKAGE_NAME || PackageUtils.isGooglePackage(activity, intent.`package`)) {
// Only allow to start Google packages
activity.startActivity(intent)
+ return true
} else {
Log.w(TAG, "Ignoring request to start non-Google app")
}
@@ -61,10 +97,21 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView
}
return false
}
+ val overrideUri = Uri.parse(url)
+ if (overrideUri.path?.endsWith("/signin/identifier") == true) {
+ val intent = Intent(activity, LoginActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
+ activity.startActivity(intent)
+ return true
+ }
+ if (overrideUri.path?.endsWith("/Logout") == true) {
+ val intent = Intent(Settings.ACTION_SYNC_SETTINGS).apply { putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf(AuthConstants.DEFAULT_ACCOUNT_TYPE)) }
+ activity.startActivity(intent)
+ return true
+ }
if (allowedPrefixes.isNotEmpty() && allowedPrefixes.none { url.startsWith(it) }) {
try {
// noinspection UnsafeImplicitIntentLaunch
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { addCategory(Intent.CATEGORY_BROWSABLE) }
+ val intent = Intent(Intent.ACTION_VIEW, overrideUri).apply { addCategory(Intent.CATEGORY_BROWSABLE) }
if (callingPackage?.let { PackageUtils.isGooglePackage(activity, it) } == true) {
try {
intent.`package` = GMS_PACKAGE_NAME
@@ -78,6 +125,7 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView
} catch (e: Exception) {
Log.w(TAG, "Error forwarding to browser", e)
}
+ activity.finish()
return true
}
return false
@@ -138,7 +186,7 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView
}
}
- private fun prepareWebViewSettings(settings: WebSettings) {
+ private fun prepareWebViewSettings(settings: WebSettings, callingPackage:String?) {
settings.javaScriptEnabled = true
settings.setSupportMultipleWindows(false)
settings.allowFileAccess = false
@@ -150,6 +198,9 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView
settings.userAgentString = "${settings.userAgentString} ${
String.format(Locale.getDefault(), "OcIdWebView (%s)", JSONObject().apply {
put("os", "Android")
+ put("osVersion", SDK_INT)
+ put("app", GMS_PACKAGE_NAME)
+ put("callingAppId", callingPackage ?: "")
}.toString())
}"
}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAdvertisingIdBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAdvertisingIdBridge.kt
new file mode 100644
index 0000000000..45b6fcb6b4
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAdvertisingIdBridge.kt
@@ -0,0 +1,26 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.content.Context
+import android.util.Log
+import android.webkit.JavascriptInterface
+import com.google.android.gms.ads.identifier.AdvertisingIdClient
+
+class OcAdvertisingIdBridge(val context: Context) {
+
+ companion object {
+ const val NAME = "ocAdvertisingId"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun getAdvertisingId(): String? {
+ Log.d(TAG, "getAdvertisingId: ")
+ return AdvertisingIdClient.getAdvertisingIdInfo(context).id
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAndroidIdBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAndroidIdBridge.kt
new file mode 100644
index 0000000000..f6531191b8
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAndroidIdBridge.kt
@@ -0,0 +1,27 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.content.Context
+import android.util.Log
+import android.webkit.JavascriptInterface
+import org.microg.gms.checkin.LastCheckinInfo
+
+class OcAndroidIdBridge(val context: Context) {
+
+ companion object {
+ const val NAME = "ocAndroidId"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun getAndroidId(): String? {
+ Log.d(TAG, "getAndroidId: ")
+ val androidId = LastCheckinInfo.read(context).androidId
+ return if (androidId != 0L) androidId.toString(16) else null
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAppBarBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAppBarBridge.kt
new file mode 100644
index 0000000000..d6e2455401
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAppBarBridge.kt
@@ -0,0 +1,132 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.text.TextUtils
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import android.widget.TextView
+import androidx.appcompat.widget.Toolbar
+import androidx.core.content.ContextCompat
+import com.google.android.gms.R
+import org.microg.gms.accountsettings.ui.runOnMainLooper
+
+class OcAppBarBridge(val toolBar: Toolbar, val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocAppBar"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun clear() {
+ Log.d(TAG, "clear: ")
+ setTitleText(null)
+ setTitleType(1)
+ setTitleFontFamily(0)
+ setStyle(1)
+ setAccountDisplay(1)
+ setUpButtonAction(1)
+ setHelpContext(null)
+ setActionMenu(null)
+ setShadowVisible(true)
+ setUpButtonVisible(true)
+ setPullToRefreshEnabled(true)
+ }
+
+ @JavascriptInterface
+ fun commitChanges() {
+ Log.d(TAG, "commitChanges: ")
+ }
+
+ @JavascriptInterface
+ fun show(id: Double?) {
+ Log.d(TAG, "show: id: $id")
+ }
+
+ @JavascriptInterface
+ fun hide(id: Double?) {
+ Log.d(TAG, "hide: id: $id")
+ }
+
+ @JavascriptInterface
+ fun isNewAppBarFeaturesSupported(): Boolean {
+ Log.d(TAG, "isNewAppBarFeaturesSupported: ")
+ return true
+ }
+
+ @JavascriptInterface
+ fun setAccountDisplay(displayId: Int?) {
+ Log.d(TAG, "setAccountDisplay: displayId: $displayId")
+ }
+
+ @JavascriptInterface
+ fun setActionMenu(base64Str: String?) {
+ Log.d(TAG, "setActionMenu: base64Str: $base64Str")
+ }
+
+ @JavascriptInterface
+ fun setHelpContext(url: String?) {
+ Log.d(TAG, "setHelpContext: url: $url")
+ }
+
+ @JavascriptInterface
+ fun setPullToRefreshEnabled(enable: Boolean?) {
+ Log.d(TAG, "setPullToRefreshEnabled: enable: $enable")
+ }
+
+ @JavascriptInterface
+ fun setShadowVisible(visible: Boolean?) {
+ Log.d(TAG, "setShadowVisible: visible: $visible")
+ }
+
+ @JavascriptInterface
+ fun setStyle(style: Int?) {
+ Log.d(TAG, "setStyle: style: $style")
+ }
+
+ @JavascriptInterface
+ fun setTitleFontFamily(family: Int?) {
+ Log.d(TAG, "setTitleFontFamily: family: $family")
+ }
+
+ @JavascriptInterface
+ fun setTitleText(title: String?) {
+ Log.d(TAG, "setTitleText: title: $title")
+ runOnMainLooper {
+ val text = if (TextUtils.isEmpty(title)) {
+ ContextCompat.getString(toolBar.context, R.string.pref_accounts_title)
+ } else title
+ toolBar.setCustomTitleText(text)
+ }
+ }
+
+ @JavascriptInterface
+ fun setTitleType(type: Int?) {
+ Log.d(TAG, "setTitleType: type: $type")
+ }
+
+ @JavascriptInterface
+ fun setUpButtonAction(action: Int?) {
+ Log.d(TAG, "setUpButtonAction: action: $action")
+ }
+
+ @JavascriptInterface
+ fun setUpButtonVisible(visible: Boolean?) {
+ Log.d(TAG, "setUpButtonVisible: visible: $visible")
+ }
+
+ private fun Toolbar.setCustomTitleText(text: String?) {
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ if (child is TextView) {
+ child.text = text
+ break
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAppPermissionsBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAppPermissionsBridge.kt
new file mode 100644
index 0000000000..6b3a1ac516
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcAppPermissionsBridge.kt
@@ -0,0 +1,38 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcAppPermissionsBridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocAppPermissions"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun getAppPermissionsData(eventId: Int?) {
+ Log.d(TAG, "getAppPermissionsData: eventId: $eventId")
+ ocAppPermissionsCallbackError(eventId)
+ }
+
+ @JavascriptInterface
+ fun getSupportedPermissionsDescription(eventId: Int?) {
+ Log.d(TAG, "getSupportedPermissionsDescription: eventId: $eventId")
+ ocAppPermissionsCallbackError(eventId)
+ }
+
+ private fun ocAppPermissionsCallbackError(eventId: Int?) {
+ val format = String.format(Locale.ROOT, "window.ocAppPermissionsCallback(%s, %s, %s)", eventId, null, true)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcClientInfoBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcClientInfoBridge.kt
new file mode 100644
index 0000000000..f3262cfdfe
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcClientInfoBridge.kt
@@ -0,0 +1,50 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import org.microg.gms.profile.Build
+import android.util.Log
+import android.webkit.JavascriptInterface
+import org.microg.gms.common.Constants
+
+class OcClientInfoBridge() {
+
+ companion object {
+ const val NAME = "ocClientInfo"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun getGmsCoreModuleApkVersionName(): String? {
+ Log.d(TAG, "getGmsCoreModuleApkVersionName: ")
+ return null
+ }
+
+ @JavascriptInterface
+ fun getGmsCoreModuleVersion(): Int {
+ Log.d(TAG, "getGmsCoreModuleVersion: ")
+ return 0
+ }
+
+ @JavascriptInterface
+ fun getGmsCoreVersion(): Int {
+ Log.d(TAG, "getGmsCoreVersion: ")
+ return Constants.GMS_VERSION_CODE
+ }
+
+ @JavascriptInterface
+ fun getOsVersion(): String? {
+ Log.d(TAG, "getOsVersion: ")
+ return Build.VERSION.RELEASE
+ }
+
+ @JavascriptInterface
+ fun getSdkVersion(): Int {
+ Log.d(TAG, "getSdkVersion: ")
+ return Build.VERSION.SDK_INT
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcConsistencyBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcConsistencyBridge.kt
new file mode 100644
index 0000000000..84e4104517
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcConsistencyBridge.kt
@@ -0,0 +1,33 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+
+class OcConsistencyBridge() {
+
+ companion object {
+ const val NAME = "ocConsistency"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun accountWasDeleted() {
+ Log.d(TAG, "accountWasDeleted: ")
+ }
+
+ @JavascriptInterface
+ fun accountWasRenamed() {
+ Log.d(TAG, "accountWasRenamed: ")
+ }
+
+ @JavascriptInterface
+ fun verifyActualAccountId(accountId: String?) {
+ Log.d(TAG, "verifyActualAccountId: accountId: $accountId")
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcContactsBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcContactsBridge.kt
new file mode 100644
index 0000000000..04a26a995a
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcContactsBridge.kt
@@ -0,0 +1,27 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcContactsBridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocContacts"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun readContacts() {
+ Log.d(TAG, "readContacts: ")
+ val format = String.format(Locale.ROOT, "window.ocContactsReadContactsCallback(%s, %s)", null, true)
+ evaluateJavascriptCallback(webView, format)
+ }
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFido2Bridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFido2Bridge.kt
new file mode 100644
index 0000000000..038b1b4d6d
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFido2Bridge.kt
@@ -0,0 +1,28 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcFido2Bridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocFido2"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun startBuiltInAuthenticatorAssertionRequest(json: String?) {
+ Log.d(TAG, "startBuiltInAuthenticatorAssertionRequest: json: $json")
+ val format = String.format(Locale.ROOT, "window.ocFido2BuiltInAuthenticatorAssertionResponse(%s)", null)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFidoU2fBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFidoU2fBridge.kt
new file mode 100644
index 0000000000..bd776e0a76
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFidoU2fBridge.kt
@@ -0,0 +1,32 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcFidoU2fBridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "mm"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun sendSkUiEvent(eventJsonStr: String?) {
+ Log.d(TAG, "sendSkUiEvent: eventJsonStr: $eventJsonStr")
+ }
+
+ @JavascriptInterface
+ fun startSecurityKeyAssertionRequest(requestJsonStr: String?) {
+ Log.d(TAG, "startSecurityKeyAssertionRequest: requestJsonStr: $requestJsonStr")
+ val format = String.format(Locale.ROOT, "window.setSkResult(%s);", null)
+ evaluateJavascriptCallback(webView, format)
+ }
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFilePickerBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFilePickerBridge.kt
new file mode 100644
index 0000000000..b9cf11dd42
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFilePickerBridge.kt
@@ -0,0 +1,121 @@
+/*
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.net.Uri
+import android.util.Base64
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.MainActivity
+import org.microg.gms.accountsettings.ui.PicturePickerUtils
+import org.microg.gms.accountsettings.ui.ResultStatus
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import org.microg.gms.accountsettings.ui.runOnMainLooper
+import java.lang.RuntimeException
+import java.util.concurrent.ExecutorService
+
+class OcFilePickerBridge(val activity: MainActivity, val webView: WebView, val executor: ExecutorService) {
+
+ companion object {
+ const val NAME = "ocFilePicker"
+ private const val TAG = "JS_$NAME"
+ }
+
+ private var currentRequestId: Int = 0
+ private var pendingRequestId: Int? = null
+ private var lastResult: Triple? = null
+
+ private val pickerUtils = PicturePickerUtils(activity, ::handleResult, ::handleError)
+
+ @JavascriptInterface
+ fun pick(requestId: Int, mimeType: String?) {
+ Log.d(TAG, "pick: requestId = $requestId, mimeType = $mimeType")
+ currentRequestId = requestId
+ val type = mimeType ?: "*/*"
+
+ runOnMainLooper {
+ try {
+ pickerUtils.launchChooser(type)
+ } catch (e: Exception) {
+ Log.w(TAG, "pick: launchChooser error", e)
+ notifyJavascript(requestId, ResultStatus.FAILED.value, "", "")
+ }
+ }
+ }
+
+ @JavascriptInterface
+ fun resume(requestId: Int) {
+ Log.d(TAG, "resume: requestId: $requestId lastResult:$lastResult")
+ val lastResult = this.lastResult
+
+ runOnMainLooper {
+ if (lastResult != null) {
+ val (status, mimeType, data) = lastResult
+ notifyJavascript(requestId, status, mimeType ?: "", data ?: "")
+ this.lastResult = null
+ } else if (pendingRequestId != null) {
+ pendingRequestId = requestId
+ } else {
+ notifyJavascript(requestId, ResultStatus.NO_OP.value, "", "")
+ }
+ }
+ }
+
+ private fun handleResult(uri: Uri?) {
+ if (uri == null) {
+ notifyJavascript(currentRequestId, ResultStatus.USER_CANCEL.value, "", "")
+ return
+ }
+ pendingRequestId = currentRequestId
+ executor.submit {
+ try {
+ val contentResolver = activity.contentResolver
+ val mimeType = contentResolver.getType(uri) ?: "image/jpeg"
+ val inputStream = contentResolver.openInputStream(uri)
+
+ if (inputStream != null) {
+ val bytes = inputStream.readBytes()
+ val encodedData = Base64.encodeToString(bytes, Base64.NO_WRAP)
+ inputStream.close()
+
+ runOnMainLooper {
+ val pendingId = pendingRequestId
+ if (pendingId != null) {
+ notifyJavascript(pendingId, ResultStatus.SUCCESS.value, mimeType, encodedData)
+ pendingRequestId = null
+ } else {
+ lastResult = Triple(ResultStatus.SUCCESS.value, mimeType, encodedData)
+ }
+ }
+ } else {
+ throw RuntimeException("Failed to open input stream")
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "handleResult: ", e)
+ runOnMainLooper {
+ val pendingId = pendingRequestId
+ if (pendingId != null) {
+ notifyJavascript(pendingId, ResultStatus.FAILED.value, "", "")
+ pendingRequestId = null
+ }
+ }
+ }
+ }
+ }
+
+ private fun handleError(status: ResultStatus) {
+ notifyJavascript(currentRequestId, status.value, "", "")
+ }
+
+ private fun notifyJavascript(requestId: Int, status: Int, mimeType: String, data: String) {
+ Log.d(TAG, "notifyJavascript: requestId: $requestId status: $status mimeType: $mimeType, data: $data")
+ val escapedData = data.replace("\\", "\\\\").replace("'", "\\'")
+ val script = "window.ocFilePickerCallback($requestId, $status, '$mimeType', '$escapedData')"
+ evaluateJavascriptCallback(webView, script)
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFolsomBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFolsomBridge.kt
new file mode 100644
index 0000000000..78679ebb3e
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcFolsomBridge.kt
@@ -0,0 +1,23 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+
+class OcFolsomBridge() {
+
+ companion object {
+ const val NAME = "ocFolsom"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun addEncryptionRecoveryMethod(key: String?, jsonArray: String?, jsonObject: String?, eventId: Int?) {
+ Log.d(TAG, "addEncryptionRecoveryMethod: key: $key, jsonArray: $jsonArray, jsonObject: $jsonObject, eventId: $eventId")
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcPermissionsBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcPermissionsBridge.kt
new file mode 100644
index 0000000000..83dc9ce246
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcPermissionsBridge.kt
@@ -0,0 +1,34 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcPermissionsBridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocPermissions"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun checkPermissions(permissionBase64: String?): String? {
+ Log.d(TAG, "checkPermissions: permissionBase64: $permissionBase64")
+ return null
+ }
+
+ @JavascriptInterface
+ fun ensurePermissions(permissionBase64: String?) {
+ Log.d(TAG, "ensurePermissions: permissionBase64: $permissionBase64")
+ val format = String.format(Locale.ROOT, "window.ocPermissionsEnsurePermissionsCallback(%s, %s)", null, true)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcPlayProtectBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcPlayProtectBridge.kt
new file mode 100644
index 0000000000..1088bdd33f
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcPlayProtectBridge.kt
@@ -0,0 +1,72 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcPlayProtectBridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocPlayProtect"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun enablePlayProtect(protectId: Int?) {
+ Log.d(TAG, "enablePlayProtect: protectId: $protectId")
+ ocPlayProtectCallback(protectId)
+ }
+
+ @JavascriptInterface
+ fun getHarmfulAppsCount(protectId: Int?) {
+ Log.d(TAG, "getHarmfulAppsCount: protectId: $protectId")
+ ocPlayProtectCallback(protectId)
+ }
+
+ @JavascriptInterface
+ fun getLastScanTimeMs(protectId: Int?) {
+ Log.d(TAG, "getLastScanTimeMs: protectId: $protectId")
+ ocPlayProtectCallback(protectId)
+ }
+
+ @JavascriptInterface
+ fun isPlayProtectEnabled(protectId: Int?) {
+ Log.d(TAG, "isPlayProtectEnabled: protectId: $protectId")
+ ocPlayProtectCallback(protectId)
+ }
+
+ @JavascriptInterface
+ fun isPlayStoreVersionValid(protectId: Int?) {
+ Log.d(TAG, "isPlayStoreVersionValid: protectId: $protectId")
+ ocPlayProtectCallback(protectId, true)
+ }
+
+ @JavascriptInterface
+ fun startPlayProtectActivity(protectId: Int?) {
+ Log.d(TAG, "startPlayProtectActivity: protectId: $protectId")
+ ocPlayProtectCallbackV2(protectId)
+ }
+
+ private fun ocPlayProtectCallback(protectId: Int?, valid: Boolean) {
+ val format = String.format(Locale.ROOT, "window.ocPlayProtectCallback(%s, %s)", protectId, valid)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+ private fun ocPlayProtectCallback(protectId: Int?) {
+ val format = String.format(Locale.ROOT, "window.ocPlayProtectCallback(%s, %s, %s)", protectId, null, true)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+ private fun ocPlayProtectCallbackV2(protectId: Int?) {
+ val format = String.format(Locale.ROOT, "window.ocPlayProtectCallback(%s)", protectId)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcTelephonyBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcTelephonyBridge.kt
new file mode 100644
index 0000000000..36fb32bd37
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcTelephonyBridge.kt
@@ -0,0 +1,68 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+
+class OcTelephonyBridge() {
+
+ companion object {
+ const val NAME = "ocTelephony"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun getPhoneNumber(): String? {
+ Log.d(TAG, "getPhoneNumber: ")
+ return null
+ }
+
+ @JavascriptInterface
+ fun getSimCountryIso(): String? {
+ Log.d(TAG, "getSimCountryIso: ")
+ return null
+ }
+
+ @JavascriptInterface
+ fun getSimState(): Int {
+ Log.d(TAG, "getSimState: ")
+ return 0
+ }
+
+ @JavascriptInterface
+ fun hasPhoneNumber(): Boolean {
+ Log.d(TAG, "hasPhoneNumber: ")
+ return false
+ }
+
+ @JavascriptInterface
+ fun hasTelephony(): Boolean {
+ Log.d(TAG, "hasTelephony: ")
+ return false
+ }
+
+ @JavascriptInterface
+ fun listenForSmsCodes() {
+ Log.d(TAG, "listenForSmsCodes: ")
+ }
+
+ @JavascriptInterface
+ fun sendSms(type: Int, contentBase64: String) {
+ Log.d(TAG, "sendSms: type: $type, contentBase64: $contentBase64")
+ }
+
+ @JavascriptInterface
+ fun sendSmsSupportedByBridge(): Boolean {
+ Log.d(TAG, "sendSmsSupportedByBridge: ")
+ return false
+ }
+
+ @JavascriptInterface
+ fun stopListeningForSmsCodes() {
+ Log.d(TAG, "stopListeningForSmsCodes: ")
+ }
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcTrustAgentBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcTrustAgentBridge.kt
new file mode 100644
index 0000000000..8d5745029e
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcTrustAgentBridge.kt
@@ -0,0 +1,72 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcTrustAgentBridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocTrustAgent"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun isScreenLockSet(agentId: Int?) {
+ Log.d(TAG, "isScreenLockSet: agentId: $agentId")
+ ocTrustAgentCallback(agentId, false)
+ }
+
+ @JavascriptInterface
+ fun isSmartLockSet(agentId: Int?) {
+ Log.d(TAG, "isSmartLockSet: agentId: $agentId")
+ ocTrustAgentCallback(agentId)
+ }
+
+ @JavascriptInterface
+ fun isSmartLockSupported(agentId: Int?) {
+ Log.d(TAG, "isSmartLockSupported: agentId: $agentId")
+ ocTrustAgentCallback(agentId, false)
+ }
+
+ @JavascriptInterface
+ fun isTrustletSet(config: String?, agentId: Int?) {
+ Log.d(TAG, "isTrustletSet: agentId: $agentId")
+ ocTrustAgentCallback(agentId)
+ }
+
+ @JavascriptInterface
+ fun isTrustletSupported(agentId: Int?) {
+ Log.d(TAG, "isTrustletSupported: agentId: $agentId")
+ ocTrustAgentCallback(agentId)
+ }
+
+ @JavascriptInterface
+ fun startScreenLockSmartLockFlow(agentId: Int?) {
+ Log.d(TAG, "startScreenLockSmartLockFlow: agentId: $agentId")
+ ocTrustAgentCallbackV2(agentId)
+ }
+
+ private fun ocTrustAgentCallback(agentId: Int?, valid: Boolean) {
+ val format = String.format(Locale.ROOT, "window.ocTrustAgentCallback(%s, %s)", agentId, valid)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+ private fun ocTrustAgentCallback(agentId: Int?) {
+ val format = String.format(Locale.ROOT, "window.ocTrustAgentCallback(%s, %s, %s)", agentId, false, true)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+ private fun ocTrustAgentCallbackV2(agentId: Int?) {
+ val format = String.format(Locale.ROOT, "window.ocTrustAgentCallback(%s)", agentId)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUdcBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUdcBridge.kt
new file mode 100644
index 0000000000..0207c61f7a
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUdcBridge.kt
@@ -0,0 +1,68 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.accounts.Account
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.microg.gms.accountsettings.ui.evaluateJavascriptCallback
+import java.util.Locale
+
+class OcUdcBridge(val webView: WebView) {
+
+ companion object {
+ const val NAME = "ocUdc"
+ private const val TAG = "JS_$NAME"
+ }
+
+ @JavascriptInterface
+ fun canGetUlrDeviceInformation(): Boolean {
+ Log.d(TAG, "canGetUlrDeviceInformation: ")
+ return false
+ }
+
+ @JavascriptInterface
+ fun canOpenUlrSettingsUi(account: Account?): Boolean {
+ Log.d(TAG, "canOpenUlrSettingsUi: account: ${account?.name}")
+ return account != null
+ }
+
+ @JavascriptInterface
+ fun getDeviceSettingsStates(iArray: IntArray, eventId: Int?) {
+ Log.d(TAG, "getDeviceSettingsStates: eventId: $eventId")
+ ocUdcCallbackError(eventId)
+ }
+
+ @JavascriptInterface
+ fun getSupportedDeviceSettings(eventId: Int?) {
+ Log.d(TAG, "getSupportedDeviceSettings: eventId: $eventId")
+ ocUdcCallbackError(eventId)
+ }
+
+ @JavascriptInterface
+ fun getUlrDeviceInformation(eventId: Int?) {
+ Log.d(TAG, "getUlrDeviceInformation: eventId: $eventId")
+ }
+
+ @JavascriptInterface
+ fun openUlrSettingsUi(): Boolean {
+ Log.d(TAG, "openUlrSettingsUi: ")
+ return false
+ }
+
+ @JavascriptInterface
+ fun setDeviceSetting(settingId: Int?, flag: Boolean?, eventId: Int?) {
+ Log.d(TAG, "setDeviceSetting: settingId: $settingId, flag: $flag, eventId: $eventId")
+ ocUdcCallbackError(eventId)
+ }
+
+ private fun ocUdcCallbackError(eventId: Int?) {
+ val format = String.format(Locale.ROOT, "window.ocUdcCallback(%s, %s, %s)", eventId, null, true)
+ evaluateJavascriptCallback(webView, format)
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt
new file mode 100644
index 0000000000..b2567f0958
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt
@@ -0,0 +1,142 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.accountsettings.ui.bridge
+
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import org.json.JSONException
+import org.json.JSONObject
+import org.microg.gms.accountsettings.ui.EXTRA_ACCOUNT_NAME
+import org.microg.gms.accountsettings.ui.EXTRA_SCREEN_ID
+import org.microg.gms.accountsettings.ui.KEY_UPDATED_PHOTO_URL
+import org.microg.gms.accountsettings.ui.MainActivity
+
+class OcUiBridge(val activity: MainActivity, val accountName:String?, val webView: WebView?) {
+
+ companion object{
+ const val NAME = "ocUi"
+ private const val TAG = "JS_$NAME"
+ }
+
+ private var resultBundle: Bundle? = null
+
+ @JavascriptInterface
+ fun close() {
+ Log.d(TAG, "close: ")
+ val intent = Intent()
+ if (resultBundle != null) {
+ intent.putExtras(resultBundle!!)
+ }
+ activity.setResult(RESULT_OK, intent)
+ activity.finish()
+ }
+
+ @JavascriptInterface
+ fun closeWithResult(resultJsonStr: String?) {
+ Log.d(TAG, "closeWithResult: resultJsonStr -> $resultJsonStr")
+ setResult(resultJsonStr)
+ close()
+ }
+
+ @JavascriptInterface
+ fun goBackOrClose() {
+ Log.d(TAG, "goBackOrClose: ")
+ activity.onBackPressed()
+ }
+
+ @JavascriptInterface
+ fun hideKeyboard() {
+ Log.d(TAG, "hideKeyboard: ")
+ }
+
+ @JavascriptInterface
+ fun isCloseWithResultSupported(): Boolean {
+ Log.d(TAG, "isCloseWithResultSupported: ")
+ return true
+ }
+
+ @JavascriptInterface
+ fun isOpenHelpEnabled(): Boolean {
+ Log.d(TAG, "isOpenHelpEnabled: ")
+ return true
+ }
+
+ @JavascriptInterface
+ fun isOpenScreenEnabled(): Boolean {
+ Log.d(TAG, "isOpenScreenEnabled: ")
+ return true
+ }
+
+ @JavascriptInterface
+ fun isSetResultSupported(): Boolean {
+ Log.d(TAG, "isSetResultSupported: ")
+ return true
+ }
+
+ @JavascriptInterface
+ fun open(str: String?) {
+ Log.d(TAG, "open: str -> $str")
+ }
+
+ @JavascriptInterface
+ fun openHelp(str: String?) {
+ Log.d(TAG, "openHelp: str -> $str")
+ }
+
+ @JavascriptInterface
+ fun openScreen(screenId: Int, str: String?) {
+ Log.d(TAG, "openScreen: screenId -> $screenId str -> $str accountName -> $accountName")
+ val intent = Intent(activity, MainActivity::class.java).apply {
+ putExtra(EXTRA_SCREEN_ID, screenId)
+ putExtra(EXTRA_ACCOUNT_NAME, accountName)
+ }
+ activity.startActivity(intent)
+ }
+
+ @JavascriptInterface
+ fun setBackStop() {
+ Log.d(TAG, "setBackStop: ")
+ webView?.clearHistory()
+ }
+
+ @JavascriptInterface
+ fun setResult(resultJsonStr: String?) {
+ Log.d(TAG, "setResult: resultJsonStr -> $resultJsonStr")
+ val map = jsonToMap(resultJsonStr) ?: return
+ if (map.containsKey(KEY_UPDATED_PHOTO_URL)) {
+ activity.updateLocalAccountAvatar(map[KEY_UPDATED_PHOTO_URL], accountName)
+ }
+ resultBundle = Bundle().apply {
+ for ((key, value) in map) {
+ putString("result.$key", value)
+ }
+ }
+ }
+
+ private fun jsonToMap(jsonStr: String?): Map? {
+ val hashMap = HashMap()
+ if (!jsonStr.isNullOrEmpty()) {
+ try {
+ val jSONObject = JSONObject(jsonStr)
+ val keys = jSONObject.keys()
+ while (keys.hasNext()) {
+ val next = keys.next()
+ val obj = jSONObject[next]
+ hashMap[next] = obj as String
+ }
+ } catch (e: JSONException) {
+ Log.d(TAG, "Unable to parse result JSON string", e)
+ return null
+ }
+ }
+ return hashMap
+ }
+
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt
index 7947944786..211a61131b 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt
@@ -5,6 +5,11 @@
package org.microg.gms.accountsettings.ui
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.webkit.WebView
+
const val ACTION_BROWSE_SETTINGS = "com.google.android.gms.accountsettings.action.BROWSE_SETTINGS"
const val ACTION_MY_ACCOUNT = "com.google.android.gms.accountsettings.MY_ACCOUNT"
const val ACTION_ACCOUNT_PREFERENCES_SETTINGS = "com.google.android.gms.accountsettings.ACCOUNT_PREFERENCES_SETTINGS"
@@ -25,4 +30,37 @@ const val EXTRA_SCREEN_KID_ONBOARDING_PARAMS = "extra.screen.kidOnboardingParams
const val KEY_UPDATED_PHOTO_URL = "updatedPhotoUrl"
-const val OPTION_SCREEN_FLAVOR = "screenFlavor"
\ No newline at end of file
+const val OPTION_SCREEN_FLAVOR = "screenFlavor"
+
+enum class ResultStatus(val value: Int) {
+ USER_CANCEL(1), FAILED(2), SUCCESS(3), NO_OP(4)
+}
+
+fun evaluateJavascriptCallback(webView: WebView, script: String) {
+ runOnMainLooper {
+ webView.evaluateJavascript(script, null)
+ }
+}
+
+fun runOnMainLooper(method: () -> Unit) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ method()
+ } else {
+ Handler(Looper.getMainLooper()).post {
+ method()
+ }
+ }
+}
+
+fun isGoogleAvatarUrl(url: String?): Boolean {
+ if (url.isNullOrBlank()) return false
+ return try {
+ val uri = Uri.parse(url)
+ val isGoogleHost = uri.host == "lh3.googleusercontent.com"
+ val isAvatarPath = uri.path?.startsWith("/a/") == true
+ val hasSizeParam = url.matches(Regex(".*=s\\d+-c-no$"))
+ isGoogleHost && isAvatarPath && hasSizeParam
+ } catch (e: Exception) {
+ false
+ }
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/res/drawable/ic_arrow_close.xml b/play-services-core/src/main/res/drawable/ic_arrow_close.xml
new file mode 100644
index 0000000000..51acf8fa9f
--- /dev/null
+++ b/play-services-core/src/main/res/drawable/ic_arrow_close.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/play-services-core/src/main/res/xml/file_provider_paths.xml b/play-services-core/src/main/res/xml/file_provider_paths.xml
new file mode 100644
index 0000000000..cf9fe6d9f6
--- /dev/null
+++ b/play-services-core/src/main/res/xml/file_provider_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file