diff --git a/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IFlagUpdateListener.aidl b/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IFlagUpdateListener.aidl
new file mode 100644
index 0000000000..3b9d5c21d4
--- /dev/null
+++ b/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IFlagUpdateListener.aidl
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.phenotype.internal;
+
+interface IFlagUpdateListener {
+ oneway void onFlagUpdateListener(in byte[] bytes) = 1;
+}
diff --git a/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IGetStorageInfoCallbacks.aidl b/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IGetStorageInfoCallbacks.aidl
new file mode 100644
index 0000000000..718fd348e3
--- /dev/null
+++ b/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IGetStorageInfoCallbacks.aidl
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.phenotype.internal;
+
+import com.google.android.gms.common.api.Status;
+
+interface IGetStorageInfoCallbacks {
+ oneway void onGetStorageInfoed(in Status status, in byte[] bytes) = 1;
+}
diff --git a/play-services-base/core/package/src/main/AndroidManifest.xml b/play-services-base/core/package/src/main/AndroidManifest.xml
index 565649c2c4..9a7aa8fb7f 100644
--- a/play-services-base/core/package/src/main/AndroidManifest.xml
+++ b/play-services-base/core/package/src/main/AndroidManifest.xml
@@ -11,13 +11,5 @@
android:authorities="${applicationId}.microg.profile"
android:exported="true"
tools:ignore="ExportedContentProvider" />
-
-
-
-
-
-
diff --git a/play-services-base/core/package/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt b/play-services-base/core/package/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt
deleted file mode 100644
index 4d33be2634..0000000000
--- a/play-services-base/core/package/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 microG Project Team
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package org.microg.gms.moduleinstall
-
-import android.os.Bundle
-import android.util.Log
-import com.google.android.gms.common.Feature
-import com.google.android.gms.common.api.CommonStatusCodes
-import com.google.android.gms.common.api.Status
-import com.google.android.gms.common.api.internal.IStatusCallback
-import com.google.android.gms.common.internal.ConnectionInfo
-import com.google.android.gms.common.internal.GetServiceRequest
-import com.google.android.gms.common.internal.IGmsCallbacks
-import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse
-import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse.AvailabilityStatus.STATUS_ALREADY_AVAILABLE
-import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse
-import com.google.android.gms.common.moduleinstall.ModuleInstallResponse
-import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest
-import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks
-import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService
-import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener
-import org.microg.gms.BaseService
-import org.microg.gms.common.GmsService
-
-private const val TAG = "ModuleInstall"
-
-class ModuleInstallService : BaseService(TAG, GmsService.MODULE_INSTALL) {
- override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
- val binder = ModuleInstallServiceImpl().asBinder()
- callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, binder, ConnectionInfo().apply {
- features = arrayOf(Feature("moduleinstall", 7))
- })
- }
-}
-
-class ModuleInstallServiceImpl : IModuleInstallService.Stub() {
- override fun areModulesAvailable(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?) {
- Log.d(TAG, "Not yet implemented: areModulesAvailable $request")
- runCatching { callbacks?.onModuleAvailabilityResponse(Status.SUCCESS, ModuleAvailabilityResponse(true, STATUS_ALREADY_AVAILABLE)) }
- }
-
- override fun installModules(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?, listener: IModuleInstallStatusListener?) {
- Log.d(TAG, "Not yet implemented: installModules $request")
- runCatching { callbacks?.onModuleInstallResponse(Status.CANCELED, ModuleInstallResponse(0, true)) }
- }
-
- override fun getInstallModulesIntent(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?) {
- Log.d(TAG, "Not yet implemented: getInstallModulesIntent $request")
- runCatching { callbacks?.onModuleInstallIntentResponse(Status.CANCELED, ModuleInstallIntentResponse(null)) }
- }
-
- override fun releaseModules(callback: IStatusCallback?, request: ApiFeatureRequest?) {
- Log.d(TAG, "Not yet implemented: releaseModules $request")
- runCatching { callback?.onResult(Status.SUCCESS) }
- }
-
- override fun unregisterListener(callback: IStatusCallback?, listener: IModuleInstallStatusListener?) {
- Log.d(TAG, "Not yet implemented: unregisterListener")
- runCatching { callback?.onResult(Status.SUCCESS) }
- }
-
-}
\ No newline at end of file
diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt
index 6a592e6f25..f125c6d3b0 100644
--- a/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt
+++ b/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt
@@ -45,6 +45,14 @@ private val KNOWN_GOOGLE_APP_CERT_HASHES = listOf(
"3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a"
)
+/**
+ * All known Google signing-certificate SHA-256 hashes (the privileged platform certs + the official-apps
+ * cert), for callers that only need to decide whether a certificate is one of Google's, independent of the
+ * per-package permission model below. Stays in sync automatically as the lists above grow.
+ */
+val KNOWN_GOOGLE_CERT_SHA256: Set =
+ (KNOWN_GOOGLE_PRIVILEGED_CERT_HASHES + KNOWN_GOOGLE_APP_CERT_HASHES).toSet()
+
// This is a subset of permissions that we grant to apps signed with an official
// Google apps certificate. Note that this has lower priority than the
// KNOWN_GOOGLE_PACKAGES list, so if any app needs more permissions than this,
diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt
index 11bf68f564..b78e968770 100644
--- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt
+++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt
@@ -326,6 +326,17 @@ object SettingsContract {
)
}
+ object DynamicModule {
+ const val ID = "dynamicmodule"
+ fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
+ fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
+ const val DYNAMIC_MODULE_ENABLED = "dynamicmodule_enabled"
+
+ val PROJECTION = arrayOf(
+ DYNAMIC_MODULE_ENABLED
+ )
+ }
+
private fun withoutCallingIdentity(f: () -> T): T {
val identity = Binder.clearCallingIdentity()
try {
diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt
index 7a5cd42314..7f0a104d2e 100644
--- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt
+++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt
@@ -25,6 +25,7 @@ import org.microg.gms.settings.SettingsContract.Gcm
import org.microg.gms.settings.SettingsContract.Location
import org.microg.gms.settings.SettingsContract.Profile
import org.microg.gms.settings.SettingsContract.SafetyNet
+import org.microg.gms.settings.SettingsContract.DynamicModule
import org.microg.gms.settings.SettingsContract.Vending
import org.microg.gms.settings.SettingsContract.WorkProfile
import org.microg.gms.settings.SettingsContract.getAuthority
@@ -85,6 +86,7 @@ class SettingsProvider : ContentProvider() {
Vending.ID -> queryVending(projection ?: Vending.PROJECTION)
WorkProfile.ID -> queryWorkProfile(projection ?: WorkProfile.PROJECTION)
GameProfile.ID -> queryGameProfile(projection ?: GameProfile.PROJECTION)
+ DynamicModule.ID -> queryDynamicModule(projection ?: DynamicModule.PROJECTION)
else -> null
}
@@ -108,6 +110,7 @@ class SettingsProvider : ContentProvider() {
Vending.ID -> updateVending(values)
WorkProfile.ID -> updateWorkProfile(values)
GameProfile.ID -> updateGameProfile(values)
+ DynamicModule.ID -> updateDynamicModule(values)
else -> return 0
}
return 1
@@ -440,6 +443,25 @@ class SettingsProvider : ContentProvider() {
editor.apply()
}
+ private fun queryDynamicModule(p: Array): Cursor = MatrixCursor(p).addRow(p) { key ->
+ when (key) {
+ DynamicModule.DYNAMIC_MODULE_ENABLED -> getSettingsBoolean(key, false)
+ else -> throw IllegalArgumentException("Unknown key: $key")
+ }
+ }
+
+ private fun updateDynamicModule(values: ContentValues) {
+ if (values.size() == 0) return
+ val editor = preferences.edit()
+ values.valueSet().forEach { (key, value) ->
+ when (key) {
+ DynamicModule.DYNAMIC_MODULE_ENABLED -> editor.putBoolean(key, value as Boolean)
+ else -> throw IllegalArgumentException("Unknown key: $key")
+ }
+ }
+ editor.apply()
+ }
+
private fun MatrixCursor.addRow(
p: Array,
valueGetter: (String) -> Any?
diff --git a/play-services-basement/src/main/aidl/com/google/android/gms/common/net/ISocketFactoryCreator.aidl b/play-services-basement/src/main/aidl/com/google/android/gms/common/net/ISocketFactoryCreator.aidl
new file mode 100644
index 0000000000..c25e5053f2
--- /dev/null
+++ b/play-services-basement/src/main/aidl/com/google/android/gms/common/net/ISocketFactoryCreator.aidl
@@ -0,0 +1,13 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.common.net;
+
+import com.google.android.gms.dynamic.IObjectWrapper;
+
+interface ISocketFactoryCreator {
+ IObjectWrapper newSocketFactory(in IObjectWrapper context, in IObjectWrapper keyManagers, in IObjectWrapper trustManagers, boolean useCache) = 1;
+ IObjectWrapper newSocketFactoryWithCacheDir(in IObjectWrapper context, in IObjectWrapper keyManagers, in IObjectWrapper trustManagers, String cacheDir) = 2;
+}
diff --git a/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl b/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl
index c6cc56e9c1..7ec23c6bea 100644
--- a/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl
+++ b/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl
@@ -4,13 +4,11 @@ import com.google.android.gms.dynamic.IObjectWrapper;
interface IDynamiteLoader {
int getModuleVersion(IObjectWrapper wrappedContext, String moduleId) = 0;
- int getModuleVersion2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) = 2;
- int getModuleVersionV2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) = 4;
- IObjectWrapper getModuleVersionV3(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired, long requestStartTime) = 6;
-
IObjectWrapper createModuleContext(IObjectWrapper wrappedContext, String moduleId, int minVersion) = 1;
- IObjectWrapper createModuleContextV2(IObjectWrapper wrappedContext, String moduleId, int minVersion) = 3;
- IObjectWrapper createModuleContextV3(IObjectWrapper wrappedContext, String moduleId, int minVersion, IObjectWrapper cursorWrapped) = 7;
-
+ int getModuleVersion2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) = 2;
+ IObjectWrapper createModuleContextNoCrashUtils(IObjectWrapper wrappedContext, String moduleId, int minVersion) = 3;
+ int getModuleVersion2NoCrashUtils(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) = 4;
int getIDynamiteLoaderVersion() = 5;
+ IObjectWrapper queryForDynamiteModuleNoCrashUtils(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired, long requestStartTime) = 6;
+ IObjectWrapper createModuleContext3NoCrashUtils(IObjectWrapper wrappedContext, String moduleId, int minVersion, IObjectWrapper cursorWrapped) = 7;
}
diff --git a/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java b/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java
index 2d708c988b..83f445ba38 100644
--- a/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java
+++ b/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java
@@ -6,11 +6,14 @@
package com.google.android.gms.dynamite;
import android.content.Context;
+import android.database.Cursor;
import android.os.IBinder;
-import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
+import com.google.android.gms.dynamic.IObjectWrapper;
+import com.google.android.gms.dynamic.ObjectWrapper;
+
import java.lang.reflect.Field;
import java.util.Objects;
@@ -21,39 +24,21 @@ public class DynamiteModule {
public static final int LOCAL = -1;
public static final int REMOTE = 1;
- @NonNull
- public static final VersionPolicy PREFER_REMOTE = (context, moduleId, versions) -> {
- VersionPolicy.SelectionResult result = new VersionPolicy.SelectionResult();
- result.remoteVersion = versions.getRemoteVersion(context, moduleId, true);
- if (result.remoteVersion != 0) {
- result.selection = REMOTE;
- } else {
- result.localVersion = versions.getLocalVersion(context, moduleId);
- if (result.localVersion != 0) {
- result.selection = LOCAL;
- }
- }
- return result;
- };
- @NonNull
- public static final VersionPolicy PREFER_LOCAL = (context, moduleId, versions) -> {
- VersionPolicy.SelectionResult result = new VersionPolicy.SelectionResult();
- result.localVersion = versions.getLocalVersion(context, moduleId);
- if (result.localVersion != 0) {
- result.selection = LOCAL;
- } else {
- result.remoteVersion = versions.getRemoteVersion(context, moduleId, true);
- if (result.remoteVersion != 0) {
- result.selection = REMOTE;
- }
- }
- return result;
- };
+ private static IDynamiteLoader sCachedLoader;
+
+ private final Context moduleContext;
+
+ private DynamiteModule(Context moduleContext) {
+ this.moduleContext = moduleContext;
+ }
+
+ public Context getModuleContext() {
+ return moduleContext;
+ }
public interface VersionPolicy {
interface IVersions {
int getLocalVersion(@NonNull Context context, @NonNull String moduleId);
-
int getRemoteVersion(@NonNull Context context, @NonNull String moduleId, boolean forceStaging) throws LoadingException;
IVersions Default = new IVersions() {
@@ -79,38 +64,49 @@ class SelectionResult {
}
public static class LoadingException extends Exception {
- public LoadingException(String message) {
- super(message);
- }
-
- public LoadingException(String message, Throwable cause) {
- super(message, cause);
- }
+ public LoadingException(String message) { super(message); }
+ public LoadingException(String message, Throwable cause) { super(message, cause); }
}
- private Context moduleContext;
-
- private DynamiteModule(Context moduleContext) {
- this.moduleContext = moduleContext;
- }
+ @NonNull
+ public static final VersionPolicy PREFER_REMOTE = (context, moduleId, versions) -> {
+ VersionPolicy.SelectionResult r = new VersionPolicy.SelectionResult();
+ r.remoteVersion = versions.getRemoteVersion(context, moduleId, false);
+ if (r.remoteVersion != 0) {
+ r.selection = REMOTE;
+ } else {
+ r.localVersion = versions.getLocalVersion(context, moduleId);
+ if (r.localVersion != 0) r.selection = LOCAL;
+ }
+ return r;
+ };
- public Context getModuleContext() {
- return moduleContext;
- }
+ @NonNull
+ public static final VersionPolicy PREFER_LOCAL = (context, moduleId, versions) -> {
+ VersionPolicy.SelectionResult r = new VersionPolicy.SelectionResult();
+ r.localVersion = versions.getLocalVersion(context, moduleId);
+ if (r.localVersion != 0) {
+ r.selection = LOCAL;
+ } else {
+ r.remoteVersion = versions.getRemoteVersion(context, moduleId, false);
+ if (r.remoteVersion != 0) r.selection = REMOTE;
+ }
+ return r;
+ };
public static int getLocalVersion(@NonNull Context context, @NonNull String moduleId) {
try {
- ClassLoader classLoader = context.getApplicationContext().getClassLoader();
- Class> clazz = classLoader.loadClass("com.google.android.gms.dynamite.descriptors." + moduleId + ".ModuleDescriptor");
- Field moduleIdField = clazz.getDeclaredField("MODULE_ID");
- Field moduleVersionField = clazz.getDeclaredField("MODULE_VERSION");
- if (!Objects.equals(moduleIdField.get(null), moduleId)) {
- Log.e(TAG, "Module descriptor id '" + moduleIdField.get(null) + "' didn't match expected id '" + moduleId + "'");
+ ClassLoader cl = context.getApplicationContext().getClassLoader();
+ Class> clazz = cl.loadClass("com.google.android.gms.dynamite.descriptors." + moduleId + ".ModuleDescriptor");
+ Field idF = clazz.getDeclaredField("MODULE_ID");
+ Field verF = clazz.getDeclaredField("MODULE_VERSION");
+ if (!Objects.equals(idF.get(null), moduleId)) {
+ Log.e(TAG, "Module descriptor id '" + idF.get(null) + "' didn't match expected id '" + moduleId + "'");
return 0;
}
- return moduleVersionField.getInt(null);
+ return verF.getInt(null);
} catch (ClassNotFoundException e) {
- Log.w(TAG, "Local module descriptor class for" + moduleId + " not found.");
+ Log.w(TAG, "Local module descriptor class for " + moduleId + " not found.");
return 0;
} catch (Exception e) {
Log.e(TAG, "Failed to load module descriptor class.", e);
@@ -123,25 +119,36 @@ public static int getRemoteVersion(@NonNull Context context, @NonNull String mod
}
public static int getRemoteVersion(@NonNull Context context, @NonNull String moduleId, boolean forceStaging) {
- Log.e(TAG, "Remote modules not yet supported");
- return 0;
+ try {
+ IDynamiteLoader loader = getIDynamiteLoader(context);
+ if (loader == null) {
+ Log.w(TAG, "Failed to create IDynamiteLoader for version check");
+ return 0;
+ }
+ return loader.getModuleVersion2NoCrashUtils(ObjectWrapper.wrap(context), moduleId, forceStaging);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to retrieve remote module version: " + e.getMessage());
+ return 0;
+ }
}
@NonNull
public static DynamiteModule load(@NonNull Context context, @NonNull VersionPolicy policy, @NonNull String moduleId) throws LoadingException {
- Context applicationContext = context.getApplicationContext();
- if (applicationContext == null) throw new LoadingException("null application Context", null);
+ Context app = context.getApplicationContext();
+ if (app == null) throw new LoadingException("null application Context");
+
try {
VersionPolicy.SelectionResult result = policy.selectModule(context, moduleId, VersionPolicy.IVersions.Default);
- Log.i(TAG, "Considering local module " + moduleId + ":" + result.localVersion + " and remote module " + moduleId + ":" + result.remoteVersion);
+ Log.i(TAG, "Considering local " + moduleId + ":" + result.localVersion + " / remote " + moduleId + ":" + result.remoteVersion);
+
switch (result.selection) {
case NONE:
- throw new LoadingException("No acceptable module " + moduleId + " found. Local version is " + result.localVersion + " and remote version is " + result.remoteVersion + ".");
+ throw new LoadingException("No acceptable module " + moduleId + " found. local=" + result.localVersion + " remote=" + result.remoteVersion);
case LOCAL:
Log.i(TAG, "Selected local version of " + moduleId);
return new DynamiteModule(context);
case REMOTE:
- throw new UnsupportedOperationException();
+ return loadRemoteModule(context, moduleId, result.remoteVersion);
default:
throw new LoadingException("VersionPolicy returned invalid code:" + result.selection);
}
@@ -152,11 +159,94 @@ public static DynamiteModule load(@NonNull Context context, @NonNull VersionPoli
}
}
+ /**
+ * Load a remote module via IDynamiteLoader.
+ * The IDynamiteLoader is loaded from GMS's APK into this process via createPackageContext.
+ * It then creates the module context using ChimeraModuleLdr which handles cross-process
+ * APK delivery via ContentProvider when direct file access is unavailable.
+ */
+ private static DynamiteModule loadRemoteModule(Context context, String moduleId, int minVersion) throws LoadingException {
+ IDynamiteLoader loader = getIDynamiteLoader(context);
+ if (loader == null) {
+ throw new LoadingException("Failed to create IDynamiteLoader from GmsCore");
+ }
+
+ try {
+ int loaderVersion = loader.getIDynamiteLoaderVersion();
+ Log.i(TAG, "IDynamiteLoader version: " + loaderVersion + " for " + moduleId);
+
+ IObjectWrapper wrappedContext = ObjectWrapper.wrap(context);
+ IObjectWrapper wrappedResult;
+
+ if (loaderVersion >= 3) {
+ // V3: query first, then create context with cursor
+ IObjectWrapper wrappedCursor = loader.queryForDynamiteModuleNoCrashUtils(
+ wrappedContext, moduleId, false, 0L);
+ Cursor cursor = (Cursor) ObjectWrapper.unwrap(wrappedCursor);
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ int availableVersion = cursor.getInt(0);
+ if (availableVersion < minVersion) {
+ Log.w(TAG, "Available version " + availableVersion + " < requested " + minVersion);
+ }
+ }
+ wrappedResult = loader.createModuleContext3NoCrashUtils(
+ wrappedContext, moduleId, minVersion, wrappedCursor);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ } else if (loaderVersion >= 2) {
+ wrappedResult = loader.createModuleContextNoCrashUtils(
+ wrappedContext, moduleId, minVersion);
+ } else {
+ wrappedResult = loader.createModuleContext(
+ wrappedContext, moduleId, minVersion);
+ }
+
+ Context moduleCtx = (Context) ObjectWrapper.unwrap(wrappedResult);
+ if (moduleCtx == null) {
+ throw new LoadingException("Failed to load remote module " + moduleId);
+ }
+ Log.i(TAG, "Remote module loaded: " + moduleId + " classLoader=" + moduleCtx.getClassLoader());
+ return new DynamiteModule(moduleCtx);
+ } catch (LoadingException le) {
+ throw le;
+ } catch (Exception e) {
+ throw new LoadingException("Failed to load remote module " + moduleId, e);
+ }
+ }
+
+ /**
+ * Get IDynamiteLoader by loading DynamiteLoaderImpl from GMS's APK into this process.
+ * Uses CONTEXT_INCLUDE_CODE to load GMS code, similar to Google's real implementation.
+ */
+ private static synchronized IDynamiteLoader getIDynamiteLoader(Context context) {
+ if (sCachedLoader != null) return sCachedLoader;
+ try {
+ // Load DynamiteLoaderImpl from GMS package into this process
+ // Flag 3 = CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY
+ Context gmsContext = context.createPackageContext(
+ "com.google.android.gms",
+ Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
+ IBinder binder = (IBinder) gmsContext.getClassLoader()
+ .loadClass("com.google.android.gms.chimera.container.DynamiteLoaderImpl")
+ .newInstance();
+ final IDynamiteLoader loader = IDynamiteLoader.Stub.asInterface(binder);
+ if (loader != null) {
+ sCachedLoader = loader;
+ return loader;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to load IDynamiteLoader from GmsCore: " + e.getMessage());
+ }
+ return null;
+ }
+
@NonNull
public IBinder instantiate(@NonNull String className) throws LoadingException {
try {
return (IBinder) this.moduleContext.getClassLoader().loadClass(className).newInstance();
- } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | RuntimeException e) {
+ } catch (Throwable e) {
throw new LoadingException("Failed to instantiate module class: " + className, e);
}
}
diff --git a/play-services-chimera-core/build.gradle b/play-services-chimera-core/build.gradle
index 3001708db6..2925178efd 100644
--- a/play-services-chimera-core/build.gradle
+++ b/play-services-chimera-core/build.gradle
@@ -14,6 +14,12 @@ dependencies {
implementation "androidx.annotation:annotation:$annotationVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
+
+ implementation project(':play-services-base')
+ implementation project(':play-services-base-core')
+ implementation project(':play-services-core-proto')
+ implementation project(':play-services-chimeraresources')
+
}
android {
diff --git a/play-services-chimera-core/src/main/AndroidManifest.xml b/play-services-chimera-core/src/main/AndroidManifest.xml
index 6d65d89c09..0d5d9b3b2f 100644
--- a/play-services-chimera-core/src/main/AndroidManifest.xml
+++ b/play-services-chimera-core/src/main/AndroidManifest.xml
@@ -4,5 +4,7 @@
~ SPDX-License-Identifier: Apache-2.0
-->
+
+
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/BoundService.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/BoundService.kt
new file mode 100644
index 0000000000..5c74bda770
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/BoundService.kt
@@ -0,0 +1,83 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.ComponentName
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.Keep
+import com.google.android.chimera.annotation.ChimeraApiVersion
+import com.google.android.chimera.config.ChimeraApkManifestReader
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.DynamicModuleSettings
+import org.microg.gms.common.Constants
+
+@ChimeraApiVersion(added = 0L)
+@Keep
+open class BoundService: ContextWrapper(null) {
+
+ companion object {
+ private const val TAG = "BoundService"
+
+ @JvmStatic
+ fun getStartIntent(context: Context, action: String): Intent? {
+ Log.d(TAG, "getStartIntent: action: $action context: $context")
+ if (!DynamicModuleSettings.isAvailable(context)) {
+ Log.d(TAG, "Dynamic modules unavailable for bound service action: $action")
+ return null
+ }
+ val intent = Intent("com.google.android.chimera.BoundService.START").setData(Uri.fromParts("chimera-action", action, null))
+ val prefix = Constants.GMS_PACKAGE_NAME
+ try {
+ val boundServiceProxy = ChimeraConfigManager.findChimeraBoundService(
+ action.removePrefix(prefix)
+ )
+ if (boundServiceProxy?.moduleChimeraName == null) {
+ Log.w(TAG, "No bound service route for action: $action")
+ return null
+ }
+ val hostClassName = "$prefix${boundServiceProxy.moduleChimeraName}"
+ val moduleId = boundServiceProxy.moduleId?.takeIf { it.isNotEmpty() }
+ if (moduleId != null) {
+ val module = ChimeraConfigManager.findModuleByModuleId(moduleId)
+ val verifiedRoute = module != null &&
+ ChimeraApkManifestReader.readVerifiedCapabilities(context, module).any { capability ->
+ capability.moduleId == moduleId &&
+ capability.boundServiceBindings.any { binding ->
+ binding.containerName == boundServiceProxy.containerName &&
+ binding.moduleChimeraName == boundServiceProxy.moduleChimeraName
+ }
+ }
+ if (!verifiedRoute) {
+ Log.w(TAG, "Bound service route is not declared by the verified module APK: $action")
+ return null
+ }
+ }
+ val hostComponent = ComponentName(context, hostClassName)
+ if (runCatching { context.packageManager.getServiceInfo(hostComponent, 0) }.isFailure) {
+ Log.w(TAG, "No host service proxy declared for bound service action: $action")
+ return null
+ }
+ intent.component = hostComponent
+ Log.d(TAG, "BoundServiceProxy intent: $intent ${intent.data?.getSchemeSpecificPart()}")
+ return intent
+ } catch (e: IndexOutOfBoundsException) {
+ Log.w(TAG, "Possible corrupt config", e);
+ return null;
+ }
+ }
+ }
+
+ override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ }
+
+ fun getBoundService(): BoundService {
+ return this
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/DynamicBroadcastReceiver.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/DynamicBroadcastReceiver.kt
new file mode 100644
index 0000000000..105a73e95b
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/DynamicBroadcastReceiver.kt
@@ -0,0 +1,13 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera
+
+import android.content.Context
+import com.google.android.chimera.annotation.ChimeraApiVersion
+
+@ChimeraApiVersion(added = 122L)
+interface DynamicBroadcastReceiver {
+ fun setModuleContext(moduleContext: Context?)
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/android/Activity.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/Activity.kt
new file mode 100644
index 0000000000..d8b0f578e2
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/Activity.kt
@@ -0,0 +1,1380 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.android
+
+import android.app.ActionBar
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.Application
+import android.app.ComponentCaller
+import android.app.Fragment
+import android.app.FragmentManager
+import android.app.LoaderManager
+import android.app.PendingIntent
+import android.app.PictureInPictureParams
+import android.app.SharedElementCallback
+import android.app.TaskStackBuilder
+import android.app.VoiceInteractor
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.content.LocusId
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.database.Cursor
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import android.media.session.MediaController
+import android.net.Uri
+import android.os.Bundle
+import android.os.OutcomeReceiver
+import android.os.PersistableBundle
+import android.os.UserHandle
+import android.transition.Scene
+import android.transition.TransitionManager
+import android.util.AttributeSet
+import android.view.ActionMode
+import android.view.ContextMenu
+import android.view.DragAndDropPermissions
+import android.view.DragEvent
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.SearchEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
+import android.widget.Toolbar
+import android.window.OnBackInvokedDispatcher
+import android.window.SplashScreen
+import androidx.annotation.Keep
+import com.google.android.chimera.component.ChimeraProxyCallback
+import com.google.android.chimera.InstanceProvider
+import com.google.android.chimera.annotation.ChimeraApiVersion
+
+const val DEFAULT_KEYS_DIALER = 1
+const val DEFAULT_KEYS_DISABLE = 0
+const val DEFAULT_KEYS_SEARCH_GLOBAL = 4
+const val DEFAULT_KEYS_SEARCH_LOCAL = 3
+const val DEFAULT_KEYS_SHORTCUT = 2
+const val RESULT_CANCELED = 0
+const val RESULT_FIRST_USER = 1
+const val RESULT_OK = -1
+
+@Keep
+abstract class Activity protected constructor(): ActivityProxyWrapper(null, null), Window.Callback, ChimeraProxyCallback, InstanceProvider {
+ companion object {
+ @JvmStatic
+ fun fromProxy(activity: android.app.Activity): Activity {
+ return (activity as IChimeraActivityProxy).getChimeraActivity()
+ }
+ }
+
+ private var chimeraActivityProxy: IChimeraActivityProxy? = null
+
+ @ChimeraApiVersion(added = 0L)
+ override fun addContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ super.addContentView(view, params)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun clearOverrideActivityTransition(v: Int) {
+ super.clearOverrideActivityTransition(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun closeContextMenu() {
+ super.closeContextMenu()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun closeOptionsMenu() {
+ super.closeOptionsMenu()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun convertFromTranslucent() {
+ super.convertFromTranslucent()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun convertToTranslucent(listener: Any?, activityOptions: ActivityOptions?): Boolean {
+ return super.convertToTranslucent(listener, activityOptions)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun createPendingResult(requestCode: Int, data: Intent, flags: Int): PendingIntent {
+ return super.createPendingResult(requestCode, data, flags)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun dispatchGenericMotionEvent(motionEvent: MotionEvent?): Boolean {
+ return super.dispatchGenericMotionEvent(motionEvent)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun dispatchKeyEvent(keyEvent: KeyEvent?): Boolean {
+ return super.dispatchKeyEvent(keyEvent)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun dispatchKeyShortcutEvent(keyEvent: KeyEvent?): Boolean {
+ return super.dispatchKeyShortcutEvent(keyEvent)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun dispatchPopulateAccessibilityEvent(accessibilityEvent: AccessibilityEvent?): Boolean {
+ return super.dispatchPopulateAccessibilityEvent(accessibilityEvent)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun dispatchTouchEvent(motionEvent: MotionEvent?): Boolean {
+ return super.dispatchTouchEvent(motionEvent)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun dispatchTrackballEvent(motionEvent: MotionEvent?): Boolean {
+ return super.dispatchTrackballEvent(motionEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun findViewById(id: Int): T? {
+ return super.findViewById(id)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun finish() {
+ super.finish()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun finishActivity(v: Int) {
+ super.finishActivity(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun finishActivityFromChild(activity: android.app.Activity, requestCode: Int) {
+ super.finishActivityFromChild(activity, requestCode)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun finishAffinity() {
+ super.finishAffinity()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun finishAfterTransition() {
+ super.finishAfterTransition()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun finishAndRemoveTask() {
+ super.finishAndRemoveTask()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun finishFromChild(activity: android.app.Activity?) {
+ super.finishFromChild(activity)
+ }
+
+ @ChimeraApiVersion(added = 0x77L)
+ override fun getActionBar(): ActionBar? {
+ return super.getActionBar()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getApplication(): Application? {
+ return super.getApplication()
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun getCaller(): ComponentCaller? {
+ return super.getCaller()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getCallingActivity(): ComponentName? {
+ return super.getCallingActivity()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getCallingPackage(): String? {
+ return super.getCallingPackage()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getChangingConfigurations(): Int {
+ return super.getChangingConfigurations()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getChimeraImpl(): Any {
+ return this
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getComponentName(): ComponentName? {
+ return super.getComponentName()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ fun getContainerActivity(): android.app.Activity {
+ return this.chimeraActivityProxy as android.app.Activity
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getContentScene(): Scene? {
+ return super.getContentScene()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getContentTransitionManager(): TransitionManager? {
+ return super.getContentTransitionManager()
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun getCurrentCaller(): ComponentCaller {
+ return super.getCurrentCaller()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getCurrentFocus(): View? {
+ return super.getCurrentFocus()
+ }
+ override fun getFragmentManager(): FragmentManager? {
+ return super.getFragmentManager()
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun getInitialCaller(): ComponentCaller {
+ return super.getInitialCaller()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getIntent(): Intent? {
+ return super.getIntent()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getLastNonConfigurationInstance(): Any? {
+ return super.getLastNonConfigurationInstance()
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun getLaunchedFromPackage(): String? {
+ return super.getLaunchedFromPackage()
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun getLaunchedFromUid(): Int {
+ return super.getLaunchedFromUid()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getLayoutInflater(): LayoutInflater {
+ return super.getLayoutInflater()
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun getLoaderManager(): LoaderManager? {
+ return super.getLoaderManager()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getLocalClassName(): String {
+ return super.getLocalClassName()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun getMaxNumPictureInPictureActions(): Int {
+ return super.getMaxNumPictureInPictureActions()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getMediaController(): MediaController? {
+ return super.getMediaController()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getMenuInflater(): MenuInflater {
+ return super.getMenuInflater()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+ return super.getOnBackInvokedDispatcher()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getParent(): android.app.Activity {
+ return super.getParent()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getParentActivityIntent(): Intent? {
+ return super.getParentActivityIntent()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getPreferences(v: Int): SharedPreferences? {
+ return super.getPreferences(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getReferrer(): Uri? {
+ return super.getReferrer()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getRequestedOrientation(): Int {
+ return super.getRequestedOrientation()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getSearchEvent(): SearchEvent? {
+ return super.getSearchEvent()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun getSplashScreen(): SplashScreen {
+ return super.getSplashScreen()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getSystemService(name: String): Any? {
+ chimeraActivityProxy?.let { proxy ->
+ return when (name) {
+ "layout_inflater", "print" -> proxy.getSystemService(name)
+ else -> super.getSystemService(name)
+ }
+ }
+ return super.getSystemService(name)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getTaskId(): Int {
+ return super.getTaskId()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getTitle(): CharSequence? {
+ return super.getTitle()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getTitleColor(): Int {
+ return super.getTitleColor()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getVoiceInteractor(): VoiceInteractor? {
+ return super.getVoiceInteractor()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getVolumeControlStream(): Int {
+ return super.getVolumeControlStream()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getWindow(): Window? {
+ return super.getWindow()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun getWindowManager(): WindowManager? {
+ return super.getWindowManager()
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun hasWindowFocus(): Boolean {
+ return super.hasWindowFocus()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun invalidateOptionsMenu() {
+ super.invalidateOptionsMenu()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun isActivityTransitionRunning(): Boolean {
+ return super.isActivityTransitionRunning()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ @Deprecated("")
+ override fun isBackgroundVisibleBehind(): Boolean {
+ return super.isBackgroundVisibleBehind()
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun isChangingConfigurations(): Boolean {
+ return super.isChangingConfigurations()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ @Deprecated("")
+ override fun isChild(): Boolean {
+ return super.isChild()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun isDestroyed(): Boolean {
+ return super.isDestroyed()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun isFinishing(): Boolean {
+ return super.isFinishing()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun isImmersive(): Boolean {
+ return super.isImmersive()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun isInMultiWindowMode(): Boolean {
+ return super.isInMultiWindowMode()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun isInPictureInPictureMode(): Boolean {
+ return super.isInPictureInPictureMode()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun isLaunchedFromBubble(): Boolean {
+ return super.isLaunchedFromBubble()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun isLocalVoiceInteractionSupported(): Boolean {
+ return super.isLocalVoiceInteractionSupported()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun isTaskRoot(): Boolean {
+ return super.isTaskRoot()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun isVoiceInteraction(): Boolean {
+ return super.isVoiceInteraction()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun isVoiceInteractionRoot(): Boolean {
+ return super.isVoiceInteractionRoot()
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun managedQuery(uri: Uri?, arr_s: Array?, s: String?, arr_s1: Array?, s1: String?): Cursor? {
+ return super.managedQuery(uri, arr_s, s, arr_s1, s1)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun moveTaskToBack(z: Boolean): Boolean {
+ return super.moveTaskToBack(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun navigateUpTo(intent: Intent?): Boolean {
+ return super.navigateUpTo(intent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun navigateUpToFromChild(activity: android.app.Activity?, intent: Intent?): Boolean {
+ return super.navigateUpToFromChild(activity, intent)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onActionModeFinished(actionMode: ActionMode?) {
+ super.onActionModeFinished(actionMode)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onActionModeStarted(actionMode: ActionMode?) {
+ super.onActionModeStarted(actionMode)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onActivityReenter(v: Int, intent: Intent?) {
+ super.onActivityReenter(v, intent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, caller: ComponentCaller) {
+ super.onActivityResult(requestCode, resultCode, data, caller)
+ }
+
+ override fun onAttachFragment(fragment: Fragment?) {
+ super.onAttachFragment(fragment)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onBackPressed() {
+ super.onBackPressed()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onBackgroundVisibleBehindChanged(z: Boolean) {
+ super.onBackgroundVisibleBehindChanged(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onChildTitleChanged(activity: android.app.Activity?, charSequence: CharSequence?) {
+ super.onChildTitleChanged(activity, charSequence)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onConfigurationChanged(configuration: Configuration) {
+ super.onConfigurationChanged(configuration)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onContentChanged() {
+ super.onContentChanged()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onContextItemSelected(menuItem: MenuItem): Boolean {
+ return super.onContextItemSelected(menuItem)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onContextMenuClosed(menu: Menu) {
+ super.onContextMenuClosed(menu)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreate(bundle: Bundle?) {
+ super.onCreate(bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreate(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ super.onCreate(bundle, persistableBundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreateContextMenu(contextMenu: ContextMenu?, view: View?, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
+ super.onCreateContextMenu(contextMenu, view, contextMenuInfo)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreateDescription(): CharSequence? {
+ return super.onCreateDescription()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreateNavigateUpTaskStack(taskStackBuilder: TaskStackBuilder?) {
+ super.onCreateNavigateUpTaskStack(taskStackBuilder)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ return super.onCreateOptionsMenu(menu)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreatePanelMenu(v: Int, menu: Menu): Boolean {
+ return super.onCreatePanelMenu(v, menu)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreatePanelView(v: Int): View? {
+ return super.onCreatePanelView(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreateThumbnail(bitmap: Bitmap?, canvas: Canvas?): Boolean {
+ return super.onCreateThumbnail(bitmap, canvas)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
+ return super.onCreateView(parent, name, context, attrs)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ return super.onCreateView(name, context, attrs)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onDestroy() {
+ super.onDestroy()
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onGenericMotionEvent(motionEvent: MotionEvent?): Boolean {
+ return super.onGenericMotionEvent(motionEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onKeyDown(v: Int, keyEvent: KeyEvent?): Boolean {
+ return super.onKeyDown(v, keyEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onKeyLongPress(v: Int, keyEvent: KeyEvent?): Boolean {
+ return super.onKeyLongPress(v, keyEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onKeyMultiple(v: Int, v1: Int, keyEvent: KeyEvent?): Boolean {
+ return super.onKeyMultiple(v, v1, keyEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onKeyShortcut(v: Int, keyEvent: KeyEvent?): Boolean {
+ return super.onKeyShortcut(v, keyEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onKeyUp(v: Int, keyEvent: KeyEvent?): Boolean {
+ return super.onKeyUp(v, keyEvent)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun onLocalVoiceInteractionStarted() {
+ super.onLocalVoiceInteractionStarted()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun onLocalVoiceInteractionStopped() {
+ super.onLocalVoiceInteractionStopped()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onMenuItemSelected(featureId: Int, menuItem: MenuItem): Boolean {
+ return super.onMenuItemSelected(featureId, menuItem)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
+ return super.onMenuOpened(featureId, menu)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onNavigateUp(): Boolean {
+ return super.onNavigateUp()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onNavigateUpFromChild(activity: android.app.Activity?): Boolean {
+ return super.onNavigateUpFromChild(activity)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun onNewIntent(intent: Intent, componentCaller: ComponentCaller) {
+ super.onNewIntent(intent, componentCaller)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
+ return super.onOptionsItemSelected(menuItem)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onOptionsMenuClosed(menu: Menu) {
+ super.onOptionsMenuClosed(menu)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onPanelClosed(featureId: Int, menu: Menu) {
+ super.onPanelClosed(featureId, menu)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onPause() {
+ super.onPause()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onPostCreate(bundle: Bundle?) {
+ super.onPostCreate(bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onPostCreate(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ super.onPostCreate(bundle, persistableBundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onPostResume() {
+ super.onPostResume()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onPrepareNavigateUpTaskStack(taskStackBuilder: TaskStackBuilder?) {
+ super.onPrepareNavigateUpTaskStack(taskStackBuilder)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ return super.onPrepareOptionsMenu(menu)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onPreparePanel(v: Int, view: View?, menu: Menu): Boolean {
+ return super.onPreparePanel(v, view, menu)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onProvideReferrer(): Uri? {
+ return super.onProvideReferrer()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray, extra: Int) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults, extra)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onRestart() {
+ super.onRestart()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onRestoreInstanceState(bundle: Bundle) {
+ super.onRestoreInstanceState(bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onRestoreInstanceState(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ super.onRestoreInstanceState(bundle, persistableBundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onResume() {
+ super.onResume()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onSaveInstanceState(bundle: Bundle) {
+ super.onSaveInstanceState(bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onSaveInstanceState(bundle: Bundle, persistableBundle: PersistableBundle) {
+ super.onSaveInstanceState(bundle, persistableBundle)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onSearchRequested(): Boolean {
+ return super.onSearchRequested()
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onSearchRequested(searchEvent: SearchEvent?): Boolean {
+ return super.onSearchRequested(searchEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onStart() {
+ super.onStart()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onStateNotSaved() {
+ super.onStateNotSaved()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onStop() {
+ super.onStop()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onTitleChanged(charSequence: CharSequence?, v: Int) {
+ super.onTitleChanged(charSequence, v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onTouchEvent(motionEvent: MotionEvent?): Boolean {
+ return super.onTouchEvent(motionEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onTrackballEvent(motionEvent: MotionEvent?): Boolean {
+ return super.onTrackballEvent(motionEvent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onUserInteraction() {
+ super.onUserInteraction()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun onVisibleBehindCanceled() {
+ super.onVisibleBehindCanceled()
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onWindowAttributesChanged(params: WindowManager.LayoutParams?) {
+ super.onWindowAttributesChanged(params)
+ }
+ @ChimeraApiVersion(added = 0L)
+ override fun onWindowFocusChanged(z: Boolean) {
+ super.onWindowFocusChanged(z)
+ }
+ override fun onWindowStartingActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return super.onWindowStartingActionMode(callback)
+ }
+ override fun onWindowStartingActionMode(callback: ActionMode.Callback?, v: Int): ActionMode? {
+ return super.onWindowStartingActionMode(callback, v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun openContextMenu(view: View?) {
+ super.openContextMenu(view)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun openOptionsMenu() {
+ super.openOptionsMenu()
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun overrideActivityTransition(v: Int, v1: Int, v2: Int) {
+ super.overrideActivityTransition(v, v1, v2)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun overrideActivityTransition(v: Int, v1: Int, v2: Int, v3: Int) {
+ super.overrideActivityTransition(v, v1, v2, v3)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun overridePendingTransition(v: Int, v1: Int) {
+ super.overridePendingTransition(v, v1)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun overridePendingTransition(v: Int, v1: Int, v2: Int) {
+ super.overridePendingTransition(v, v1, v2)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun postponeEnterTransition() {
+ super.postponeEnterTransition()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun recreate() {
+ super.recreate()
+ }
+
+ override fun registerActivityLifecycleCallbacks(callbacks: Application.ActivityLifecycleCallbacks) {
+ super.registerActivityLifecycleCallbacks(callbacks)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun registerForContextMenu(view: View?) {
+ super.registerForContextMenu(view)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun releaseInstance(): Boolean {
+ return super.releaseInstance()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun reportFullyDrawn() {
+ super.reportFullyDrawn()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun requestDragAndDropPermissions(dragEvent: DragEvent?): DragAndDropPermissions? {
+ return super.requestDragAndDropPermissions(dragEvent)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun requestFullscreenMode(request: Int, approvalCallback: OutcomeReceiver?) {
+ super.requestFullscreenMode(request, approvalCallback)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun requestPermissions(permissions: Array, requestCode: Int) {
+ super.requestPermissions(permissions, requestCode)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun requestPermissions(permissions: Array, requestCode: Int, userId: Int) {
+ super.requestPermissions(permissions, requestCode, userId)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun requestVisibleBehind(z: Boolean): Boolean {
+ return super.requestVisibleBehind(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun requestWindowFeature(v: Int): Boolean {
+ return super.requestWindowFeature(v)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun requireViewById(v: Int): View? {
+ return super.requireViewById(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun runOnUiThread(runnable: Runnable?) {
+ super.runOnUiThread(runnable)
+ }
+
+ @ChimeraApiVersion(added = 0x77L)
+ override fun setActionBar(toolbar: Toolbar?) {
+ super.setActionBar(toolbar)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun setAllowCrossUidActivitySwitchFromBelow(z: Boolean) {
+ super.setAllowCrossUidActivitySwitchFromBelow(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setContentTransitionManager(transitionManager: TransitionManager?) {
+ super.setContentTransitionManager(transitionManager)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setContentView(v: Int) {
+ super.setContentView(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setContentView(view: View?) {
+ super.setContentView(view)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ super.setContentView(view, params)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setDefaultKeyMode(v: Int) {
+ super.setDefaultKeyMode(v)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setEnterSharedElementCallback(sharedElementCallback: SharedElementCallback?) {
+ super.setEnterSharedElementCallback(sharedElementCallback)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setExitSharedElementCallback(sharedElementCallback: SharedElementCallback?) {
+ super.setExitSharedElementCallback(sharedElementCallback)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setFeatureDrawable(v: Int, drawable: Drawable?) {
+ super.setFeatureDrawable(v, drawable)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setFeatureDrawableAlpha(v: Int, v1: Int) {
+ super.setFeatureDrawableAlpha(v, v1)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setFeatureDrawableResource(v: Int, v1: Int) {
+ super.setFeatureDrawableResource(v, v1)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setFeatureDrawableUri(v: Int, uri: Uri?) {
+ super.setFeatureDrawableUri(v, uri)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setFinishOnTouchOutside(z: Boolean) {
+ super.setFinishOnTouchOutside(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setImmersive(z: Boolean) {
+ super.setImmersive(z)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setInheritShowWhenLocked(z: Boolean) {
+ super.setInheritShowWhenLocked(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setIntent(intent: Intent?) {
+ super.setIntent(intent)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun setIntent(intent: Intent?, componentCaller: ComponentCaller?) {
+ super.setIntent(intent, componentCaller)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setLocusContext(locusId: LocusId?, bundle: Bundle?) {
+ super.setLocusContext(locusId, bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setMediaController(mediaController: MediaController?) {
+ super.setMediaController(mediaController)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setPictureInPictureParams(pictureInPictureParams: PictureInPictureParams) {
+ super.setPictureInPictureParams(pictureInPictureParams)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setProgress(v: Int) {
+ super.setProgress(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setProgressBarIndeterminate(z: Boolean) {
+ super.setProgressBarIndeterminate(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setProgressBarIndeterminateVisibility(z: Boolean) {
+ super.setProgressBarIndeterminateVisibility(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setProgressBarVisibility(z: Boolean) {
+ super.setProgressBarVisibility(z)
+ }
+
+ override fun setProxyCallbacks(arg1: Any?, arg2: Context?) {
+ this.setProxyCallbacks((arg1 as IChimeraActivityProxy?), arg2)
+ }
+
+ fun setProxyCallbacks(proxy: IChimeraActivityProxy?, context: Context?) {
+ this.chimeraActivityProxy = proxy
+ super.setProxyCallbacks((proxy as IActivityProxy), context)
+ this.attachBaseContext(context)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setRecentsScreenshotEnabled(z: Boolean) {
+ super.setRecentsScreenshotEnabled(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setRequestedOrientation(v: Int) {
+ super.setRequestedOrientation(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setResult(v: Int) {
+ super.setResult(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setResult(v: Int, intent: Intent?) {
+ super.setResult(v, intent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setSecondaryProgress(v: Int) {
+ super.setSecondaryProgress(v)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setShouldDockBigOverlays(z: Boolean) {
+ super.setShouldDockBigOverlays(z)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setShowWhenLocked(z: Boolean) {
+ super.setShowWhenLocked(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setTaskDescription(taskDescription: ActivityManager.TaskDescription?) {
+ super.setTaskDescription(taskDescription)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setTitle(v: Int) {
+ super.setTitle(v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setTitle(charSequence: CharSequence?) {
+ super.setTitle(charSequence)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setTitleColor(v: Int) {
+ super.setTitleColor(v)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setTranslucent(z: Boolean): Boolean {
+ return super.setTranslucent(z)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setTurnScreenOn(z: Boolean) {
+ super.setTurnScreenOn(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setVisible(z: Boolean) {
+ super.setVisible(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun setVolumeControlStream(v: Int) {
+ super.setVolumeControlStream(v)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun setVrModeEnabled(enabled: Boolean, requestedComponent: ComponentName) {
+ super.setVrModeEnabled(enabled, requestedComponent)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun shouldDockBigOverlays(): Boolean {
+ return super.shouldDockBigOverlays()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
+ return super.shouldShowRequestPermissionRationale(permission)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun shouldShowRequestPermissionRationale(permission: String, userId: Int): Boolean {
+ return super.shouldShowRequestPermissionRationale(permission, userId)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun shouldUpRecreateTask(intent: Intent?): Boolean {
+ return super.shouldUpRecreateTask(intent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun showAssist(bundle: Bundle?): Boolean {
+ return super.showAssist(bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun showLockTaskEscapeMessage() {
+ super.showLockTaskEscapeMessage()
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun startActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return super.startActionMode(callback)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun startActionMode(callback: ActionMode.Callback?, v: Int): ActionMode? {
+ return super.startActionMode(callback, v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivities(intents: Array) {
+ super.startActivities(intents)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivities(intents: Array, options: Bundle?) {
+ super.startActivities(intents, options)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivity(intent: Intent?) {
+ super.startActivity(intent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivity(intent: Intent?, bundle: Bundle?) {
+ super.startActivity(intent, bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivityForResult(intent: Intent?, v: Int) {
+ super.startActivityForResult(intent, v)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivityForResult(intent: Intent?, v: Int, bundle: Bundle?) {
+ super.startActivityForResult(intent, v, bundle)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun startActivityForResultAsUser(intent: Intent?, v: Int, bundle: Bundle?, userHandle: UserHandle?) {
+ super.startActivityForResultAsUser(intent, v, bundle, userHandle)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun startActivityForResultAsUser(intent: Intent?, v: Int, userHandle: UserHandle?) {
+ super.startActivityForResultAsUser(intent, v, userHandle)
+ }
+
+ @ChimeraApiVersion(added = 0x8DL)
+ override fun startActivityForResultAsUser(intent: Intent?, s: String?, v: Int, bundle: Bundle?, userHandle: UserHandle?) {
+ super.startActivityForResultAsUser(intent, s, v, bundle, userHandle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ @Deprecated("")
+ override fun startActivityFromChild(child: android.app.Activity, intent: Intent?, requestCode: Int) {
+ super.startActivityFromChild(child, intent, requestCode)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ @Deprecated("")
+ override fun startActivityFromChild(activity: android.app.Activity, intent: Intent?, v: Int, bundle: Bundle?) {
+ super.startActivityFromChild(activity, intent, v, bundle)
+ }
+
+ @Deprecated("")
+ override fun startActivityFromFragment(fragment: Fragment, intent: Intent?, v: Int) {
+ super.startActivityFromFragment(fragment, intent, v)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ @Deprecated("")
+ override fun startActivityFromFragment(fragment: Fragment, intent: Intent?, v: Int, bundle: Bundle?) {
+ super.startActivityFromFragment(fragment, intent, v, bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivityIfNeeded(intent: Intent, requestCode: Int): Boolean {
+ return super.startActivityIfNeeded(intent, requestCode)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startActivityIfNeeded(intent: Intent, requestCode: Int, options: Bundle?): Boolean {
+ return super.startActivityIfNeeded(intent, requestCode, options)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ super.startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ super.startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ super.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ super.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ @Deprecated("")
+ override fun startIntentSenderFromChild(activity: android.app.Activity?, intentSender: IntentSender?, v: Int, intent: Intent?, v1: Int, v2: Int, v3: Int) {
+ super.startIntentSenderFromChild(activity, intentSender, v, intent, v1, v2, v3)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ @Deprecated("")
+ override fun startIntentSenderFromChild(activity: android.app.Activity?, intentSender: IntentSender?, v: Int, intent: Intent?, v1: Int, v2: Int, v3: Int, bundle: Bundle?) {
+ super.startIntentSenderFromChild(activity, intentSender, v, intent, v1, v2, v3, bundle)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun startLocalVoiceInteraction(bundle: Bundle?) {
+ super.startLocalVoiceInteraction(bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startLockTask() {
+ super.startLockTask()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startManagingCursor(cursor: Cursor?) {
+ super.startManagingCursor(cursor)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startNextMatchingActivity(intent: Intent): Boolean {
+ return super.startNextMatchingActivity(intent)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startNextMatchingActivity(intent: Intent, bundle: Bundle?): Boolean {
+ return super.startNextMatchingActivity(intent, bundle)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startPostponedEnterTransition() {
+ super.startPostponedEnterTransition()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun startSearch(s: String?, z: Boolean, bundle: Bundle?, z1: Boolean) {
+ super.startSearch(s, z, bundle, z1)
+ }
+
+ @ChimeraApiVersion(added = 0x85L)
+ override fun stopLocalVoiceInteraction() {
+ super.stopLocalVoiceInteraction()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun stopLockTask() {
+ super.stopLockTask()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun stopManagingCursor(cursor: Cursor?) {
+ super.stopManagingCursor(cursor)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun takeKeyEvents(z: Boolean) {
+ super.takeKeyEvents(z)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun triggerSearch(s: String?, bundle: Bundle?) {
+ super.triggerSearch(s, bundle)
+ }
+
+ override fun unregisterActivityLifecycleCallbacks(callbacks: Application.ActivityLifecycleCallbacks) {
+ super.unregisterActivityLifecycleCallbacks(callbacks)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ override fun unregisterForContextMenu(view: View?) {
+ super.unregisterForContextMenu(view)
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/android/ActivityProxyWrapper.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/ActivityProxyWrapper.kt
new file mode 100644
index 0000000000..cbb2e5a828
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/ActivityProxyWrapper.kt
@@ -0,0 +1,2324 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.android
+
+import android.app.ActionBar
+import android.app.Activity
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.Application
+import android.app.ComponentCaller
+import android.app.Fragment
+import android.app.FragmentManager
+import android.app.LoaderManager
+import android.app.PendingIntent
+import android.app.PictureInPictureParams
+import android.app.SharedElementCallback
+import android.app.TaskStackBuilder
+import android.app.VoiceInteractor
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.content.LocusId
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.database.Cursor
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import android.media.session.MediaController
+import android.net.Uri
+import android.os.Bundle
+import android.os.OutcomeReceiver
+import android.os.PersistableBundle
+import android.os.UserHandle
+import android.transition.Scene
+import android.transition.TransitionManager
+import android.util.AttributeSet
+import android.view.ActionMode
+import android.view.ContextMenu
+import android.view.DragAndDropPermissions
+import android.view.DragEvent
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.SearchEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
+import android.widget.Toolbar
+import android.window.OnBackInvokedDispatcher
+import android.window.SplashScreen
+import com.google.android.chimera.context.ContextThemeWrapper
+import java.lang.Deprecated
+
+open class ActivityProxyWrapper: ContextThemeWrapper {
+ private lateinit var activityProxy: IActivityProxy
+
+ protected constructor(context: Context?) : super(context)
+
+ constructor(base: Context?, themeResId: Int) : super(base, themeResId)
+
+ constructor(base: Context?, theme: Resources.Theme?) : super(base, theme)
+
+ open fun addContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ activityProxy.platform_addContentView(view, params)
+ }
+
+ open fun clearOverrideActivityTransition(v: Int) {
+ activityProxy.platform_clearOverrideActivityTransition(v)
+ }
+
+ open fun closeContextMenu() {
+ activityProxy.platform_closeContextMenu()
+ }
+
+ open fun closeOptionsMenu() {
+ activityProxy.platform_closeOptionsMenu()
+ }
+
+ open fun convertFromTranslucent() {
+ activityProxy.platform_convertFromTranslucent()
+ }
+
+ open fun convertToTranslucent(
+ listener: Any?,
+ activityOptions: ActivityOptions?
+ ): Boolean {
+ return activityProxy.platform_convertToTranslucent(listener, activityOptions)
+ }
+
+ open fun createPendingResult(requestCode: Int, data: Intent, flags: Int): PendingIntent {
+ return activityProxy.platform_createPendingResult(requestCode, data, flags)
+ }
+
+ open fun dispatchGenericMotionEvent(motionEvent: MotionEvent?): Boolean {
+ return activityProxy.platform_dispatchGenericMotionEvent(motionEvent)
+ }
+
+ open fun dispatchKeyEvent(keyEvent: KeyEvent?): Boolean {
+ return activityProxy.platform_dispatchKeyEvent(keyEvent)
+ }
+
+ open fun dispatchKeyShortcutEvent(keyEvent: KeyEvent?): Boolean {
+ return activityProxy.platform_dispatchKeyShortcutEvent(keyEvent)
+ }
+
+ open fun dispatchPopulateAccessibilityEvent(accessibilityEvent: AccessibilityEvent?): Boolean {
+ return activityProxy.platform_dispatchPopulateAccessibilityEvent(accessibilityEvent)
+ }
+
+ open fun dispatchTouchEvent(motionEvent: MotionEvent?): Boolean {
+ return activityProxy.platform_dispatchTouchEvent(motionEvent)
+ }
+
+ open fun dispatchTrackballEvent(motionEvent: MotionEvent?): Boolean {
+ return activityProxy.platform_dispatchTrackballEvent(motionEvent)
+ }
+
+ open fun findViewById(id: Int): T? {
+ return this.activityProxy.platform_findViewById(id)
+ }
+
+ fun public_findViewById(id: Int): T? {
+ return this.findViewById(id)
+ }
+
+ open fun finish() {
+ activityProxy.platform_finish()
+ }
+
+ open fun finishActivity(v: Int) {
+ activityProxy.platform_finishActivity(v)
+ }
+
+ @Deprecated
+ open fun finishActivityFromChild(activity: Activity, requestCode: Int) {
+ activityProxy.platform_finishActivityFromChild(activity, requestCode)
+ }
+
+ open fun finishAffinity() {
+ activityProxy.platform_finishAffinity()
+ }
+
+ open fun finishAfterTransition() {
+ activityProxy.platform_finishAfterTransition()
+ }
+
+ open fun finishAndRemoveTask() {
+ activityProxy.platform_finishAndRemoveTask()
+ }
+
+ @Deprecated
+ open fun finishFromChild(activity: Activity?) {
+ activityProxy.platform_finishFromChild(activity)
+ }
+
+ open fun getActionBar(): ActionBar? {
+ return activityProxy.platform_getActionBar()
+ }
+
+ open fun getApplication(): Application? {
+ return this.activityProxy.platform_getApplication()
+ }
+
+ open fun getCaller(): ComponentCaller? {
+ return this.activityProxy.platform_getCaller()
+ }
+
+ open fun getCallingActivity(): ComponentName? {
+ return this.activityProxy.platform_getCallingActivity()
+ }
+
+ open fun getCallingPackage(): String? {
+ return this.activityProxy.platform_getCallingPackage()
+ }
+
+ open fun getChangingConfigurations(): Int {
+ return this.activityProxy.platform_getChangingConfigurations()
+ }
+
+ open fun getComponentName(): ComponentName? {
+ return this.activityProxy.platform_getComponentName()
+ }
+
+ open fun getContentScene(): Scene? {
+ return this.activityProxy.platform_getContentScene()
+ }
+
+ open fun getContentTransitionManager(): TransitionManager? {
+ return this.activityProxy.platform_getContentTransitionManager()
+ }
+
+ open fun getCurrentCaller(): ComponentCaller {
+ return this.activityProxy.platform_getCurrentCaller()
+ }
+
+ open fun getCurrentFocus(): View? {
+ return this.activityProxy.platform_getCurrentFocus()
+ }
+
+ @Deprecated
+ open fun getFragmentManager(): FragmentManager? {
+ return this.activityProxy.platform_getFragmentManager()
+ }
+
+ open fun getInitialCaller(): ComponentCaller {
+ return this.activityProxy.platform_getInitialCaller()
+ }
+
+ open fun getIntent(): Intent? {
+ return this.activityProxy.platform_getIntent()
+ }
+
+ open fun getLastNonConfigurationInstance(): Any? {
+ return activityProxy.platform_getLastNonConfigurationInstance()
+ }
+
+ open fun getLaunchedFromPackage(): String? {
+ return activityProxy.platform_getLaunchedFromPackage()
+ }
+
+ open fun getLaunchedFromUid(): Int {
+ return activityProxy.platform_getLaunchedFromUid()
+ }
+
+ open fun getLayoutInflater(): LayoutInflater {
+ return activityProxy.platform_getLayoutInflater()
+ }
+
+ @Deprecated
+ open fun getLoaderManager(): LoaderManager? {
+ return activityProxy.platform_getLoaderManager()
+ }
+
+ open fun getLocalClassName(): String {
+ return activityProxy.platform_getLocalClassName()
+ }
+
+ open fun getMaxNumPictureInPictureActions(): Int {
+ return activityProxy.platform_getMaxNumPictureInPictureActions()
+ }
+
+ open fun getMediaController(): MediaController? {
+ return activityProxy.platform_getMediaController()
+ }
+
+ open fun setMediaController(mediaController: MediaController?) {
+ activityProxy.platform_setMediaController(mediaController)
+ }
+
+ open fun getMenuInflater(): MenuInflater {
+ return activityProxy.platform_getMenuInflater()
+ }
+
+ open fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+ return activityProxy.platform_getOnBackInvokedDispatcher()
+ }
+
+ @Deprecated
+ open fun getParent(): Activity {
+ return activityProxy.platform_getParent()
+ }
+
+ open fun getParentActivityIntent(): Intent? {
+ return activityProxy.platform_getParentActivityIntent()
+ }
+
+ open fun getPreferences(v: Int): SharedPreferences? {
+ return activityProxy.platform_getPreferences(v)
+ }
+ open fun getReferrer(): Uri? {
+ return activityProxy.platform_getReferrer()
+ }
+
+ open fun getRequestedOrientation(): Int {
+ return activityProxy.platform_getRequestedOrientation()
+ }
+
+ open fun setRequestedOrientation(v: Int) {
+ activityProxy.platform_setRequestedOrientation(v)
+ }
+
+ open fun getSearchEvent(): SearchEvent? {
+ return activityProxy.platform_getSearchEvent()
+ }
+
+ open fun getSplashScreen(): SplashScreen {
+ return activityProxy.platform_getSplashScreen()
+ }
+
+ open fun getTaskId(): Int {
+ return activityProxy.platform_getTaskId()
+ }
+
+ open fun getTitle(): CharSequence? {
+ return activityProxy.platform_getTitle()
+ }
+
+ @Deprecated
+ open fun getTitleColor(): Int {
+ return activityProxy.platform_getTitleColor()
+ }
+
+ open fun setTitleColor(v: Int) {
+ activityProxy.platform_setTitleColor(v)
+ }
+
+ open fun getVoiceInteractor(): VoiceInteractor? {
+ return activityProxy.platform_getVoiceInteractor()
+ }
+
+ open fun getVolumeControlStream(): Int {
+ return activityProxy.platform_getVolumeControlStream()
+ }
+
+ open fun setVolumeControlStream(v: Int) {
+ activityProxy.platform_setVolumeControlStream(v)
+ }
+
+ open fun getWindow(): Window? {
+ return activityProxy.platform_getWindow()
+ }
+
+ open fun getWindowManager(): WindowManager? {
+ return activityProxy.platform_getWindowManager()
+ }
+
+ open fun hasWindowFocus(): Boolean {
+ return activityProxy.platform_hasWindowFocus()
+ }
+
+ open fun invalidateOptionsMenu() {
+ activityProxy.platform_invalidateOptionsMenu()
+ }
+ open fun isActivityTransitionRunning(): Boolean {
+ return activityProxy.platform_isActivityTransitionRunning()
+ }
+
+ @Deprecated
+ open fun isBackgroundVisibleBehind(): Boolean {
+ return activityProxy.platform_isBackgroundVisibleBehind()
+ }
+
+ open fun isChangingConfigurations(): Boolean {
+ return activityProxy.platform_isChangingConfigurations()
+ }
+ @Deprecated
+ open fun isChild(): Boolean {
+ return activityProxy.platform_isChild()
+ }
+
+ open fun isDestroyed(): Boolean {
+ return activityProxy.platform_isDestroyed()
+ }
+
+ open fun isFinishing(): Boolean {
+ return activityProxy.platform_isFinishing()
+ }
+
+ open fun isImmersive(): Boolean {
+ return activityProxy.platform_isImmersive()
+ }
+
+ open fun setImmersive(z: Boolean) {
+ activityProxy.platform_setImmersive(z)
+ }
+
+ open fun isInMultiWindowMode(): Boolean {
+ return activityProxy.platform_isInMultiWindowMode()
+ }
+
+ open fun isInPictureInPictureMode(): Boolean {
+ return activityProxy.platform_isInPictureInPictureMode()
+ }
+
+ open fun isLaunchedFromBubble(): Boolean {
+ return activityProxy.platform_isLaunchedFromBubble()
+ }
+
+ open fun isLocalVoiceInteractionSupported(): Boolean {
+ return activityProxy.platform_isLocalVoiceInteractionSupported()
+ }
+
+ open fun isTaskRoot(): Boolean {
+ return activityProxy.platform_isTaskRoot()
+ }
+
+ open fun isVoiceInteraction(): Boolean {
+ return activityProxy.platform_isVoiceInteraction()
+ }
+
+ open fun isVoiceInteractionRoot(): Boolean {
+ return activityProxy.platform_isVoiceInteractionRoot()
+ }
+
+ @Deprecated
+ open fun managedQuery(
+ uri: Uri?,
+ arr_s: Array?,
+ s: String?,
+ arr_s1: Array?,
+ s1: String?
+ ): Cursor? {
+ return activityProxy.platform_managedQuery(uri, arr_s, s, arr_s1, s1)
+ }
+
+ open fun moveTaskToBack(z: Boolean): Boolean {
+ return activityProxy.platform_moveTaskToBack(z)
+ }
+
+ open fun navigateUpTo(intent: Intent?): Boolean {
+ return activityProxy.platform_navigateUpTo(intent)
+ }
+
+ @Deprecated
+ open fun navigateUpToFromChild(activity: Activity?, intent: Intent?): Boolean {
+ return activityProxy.platform_navigateUpToFromChild(activity, intent)
+ }
+
+ open fun onActionModeFinished(actionMode: ActionMode?) {
+ activityProxy.platform_onActionModeFinished(actionMode)
+ }
+
+ open fun onActionModeStarted(actionMode: ActionMode?) {
+ activityProxy.platform_onActionModeStarted(actionMode)
+ }
+
+ open fun onActivityReenter(v: Int, intent: Intent?) {
+ activityProxy.platform_onActivityReenter(v, intent)
+ }
+
+ protected open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ activityProxy.platform_onActivityResult(requestCode, resultCode, data)
+ }
+
+ open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, caller: ComponentCaller) {
+ activityProxy.platform_onActivityResult(requestCode, resultCode, data, caller)
+ }
+
+ @Deprecated
+ open fun onAttachFragment(fragment: Fragment?) {
+ activityProxy.platform_onAttachFragment(fragment)
+ }
+
+ open fun onAttachedToWindow() {
+ activityProxy.platform_onAttachedToWindow()
+ }
+
+ @Deprecated
+ open fun onBackPressed() {
+ activityProxy.platform_onBackPressed()
+ }
+
+ @Deprecated
+ open fun onBackgroundVisibleBehindChanged(z: Boolean) {
+ activityProxy.platform_onBackgroundVisibleBehindChanged(z)
+ }
+
+ protected open fun onChildTitleChanged(
+ activity: Activity?,
+ charSequence: CharSequence?
+ ) {
+ activityProxy.platform_onChildTitleChanged(activity, charSequence)
+ }
+
+ open fun onConfigurationChanged(configuration: Configuration) {
+ activityProxy.platform_onConfigurationChanged(configuration)
+ }
+
+ open fun onContentChanged() {
+ activityProxy.platform_onContentChanged()
+ }
+
+ open fun onContextItemSelected(menuItem: MenuItem): Boolean {
+ return activityProxy.platform_onContextItemSelected(menuItem)
+ }
+
+ open fun onContextMenuClosed(menu: Menu) {
+ activityProxy.platform_onContextMenuClosed(menu)
+ }
+
+ protected open fun onCreate(bundle: Bundle?) {
+ activityProxy.platform_onCreate(bundle)
+ }
+
+ open fun onCreate(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ activityProxy.platform_onCreate(bundle, persistableBundle)
+ }
+
+ open fun onCreateContextMenu(
+ contextMenu: ContextMenu?,
+ view: View?,
+ contextMenuInfo: ContextMenu.ContextMenuInfo?
+ ) {
+ activityProxy.platform_onCreateContextMenu(
+ contextMenu,
+ view,
+ contextMenuInfo
+ )
+ }
+
+ open fun onCreateDescription(): CharSequence? {
+ return activityProxy.platform_onCreateDescription()
+ }
+
+ open fun onCreateNavigateUpTaskStack(taskStackBuilder: TaskStackBuilder?) {
+ activityProxy.platform_onCreateNavigateUpTaskStack(taskStackBuilder)
+ }
+
+ open fun onCreateOptionsMenu(menu: Menu): Boolean {
+ return activityProxy.platform_onCreateOptionsMenu(menu)
+ }
+
+ open fun onCreatePanelMenu(v: Int, menu: Menu): Boolean {
+ return activityProxy.platform_onCreatePanelMenu(v, menu)
+ }
+
+ open fun onCreatePanelView(v: Int): View? {
+ return activityProxy.platform_onCreatePanelView(v)
+ }
+
+ @Deprecated
+ open fun onCreateThumbnail(bitmap: Bitmap?, canvas: Canvas?): Boolean {
+ return activityProxy.platform_onCreateThumbnail(bitmap, canvas)
+ }
+
+ open fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
+ return activityProxy.platform_onCreateView(parent, name, context, attrs)
+ }
+
+ open fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ return activityProxy.platform_onCreateView(name, context, attrs)
+ }
+
+ protected open fun onDestroy() {
+ activityProxy.platform_onDestroy()
+ }
+
+ open fun onDetachedFromWindow() {
+ activityProxy.platform_onDetachedFromWindow()
+ }
+
+ open fun onGenericMotionEvent(motionEvent: MotionEvent?): Boolean {
+ return activityProxy.platform_onGenericMotionEvent(motionEvent)
+ }
+
+ open fun onKeyDown(v: Int, keyEvent: KeyEvent?): Boolean {
+ return activityProxy.platform_onKeyDown(v, keyEvent)
+ }
+
+ open fun onKeyLongPress(v: Int, keyEvent: KeyEvent?): Boolean {
+ return activityProxy.platform_onKeyLongPress(v, keyEvent)
+ }
+
+ open fun onKeyMultiple(v: Int, v1: Int, keyEvent: KeyEvent?): Boolean {
+ return activityProxy.platform_onKeyMultiple(v, v1, keyEvent)
+ }
+
+ open fun onKeyShortcut(v: Int, keyEvent: KeyEvent?): Boolean {
+ return activityProxy.platform_onKeyShortcut(v, keyEvent)
+ }
+
+ open fun onKeyUp(v: Int, keyEvent: KeyEvent?): Boolean {
+ return activityProxy.platform_onKeyUp(v, keyEvent)
+ }
+
+ open fun onLocalVoiceInteractionStarted() {
+ activityProxy.platform_onLocalVoiceInteractionStarted()
+ }
+
+ open fun onLocalVoiceInteractionStopped() {
+ activityProxy.platform_onLocalVoiceInteractionStopped()
+ }
+
+ open fun onMenuItemSelected(featureId: Int, menuItem: MenuItem): Boolean {
+ return activityProxy.platform_onMenuItemSelected(featureId, menuItem)
+ }
+
+ open fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
+ return activityProxy.platform_onMenuOpened(featureId, menu)
+ }
+
+ open fun onNavigateUp(): Boolean {
+ return activityProxy.platform_onNavigateUp()
+ }
+
+ @Deprecated
+ open fun onNavigateUpFromChild(activity: Activity?): Boolean {
+ return activityProxy.platform_onNavigateUpFromChild(activity)
+ }
+
+ protected open fun onNewIntent(intent: Intent?) {
+ activityProxy.platform_onNewIntent(intent)
+ }
+
+ open fun onNewIntent(intent: Intent, componentCaller: ComponentCaller) {
+ activityProxy.platform_onNewIntent(intent, componentCaller)
+ }
+
+ open fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
+ return activityProxy.platform_onOptionsItemSelected(menuItem)
+ }
+
+ open fun onOptionsMenuClosed(menu: Menu) {
+ activityProxy.platform_onOptionsMenuClosed(menu)
+ }
+
+ open fun onPanelClosed(featureId: Int, menu: Menu) {
+ activityProxy.platform_onPanelClosed(featureId, menu)
+ }
+
+ protected open fun onPause() {
+ activityProxy.platform_onPause()
+ }
+
+ private fun onPointerCaptureChanged(hasCapture: Boolean) {
+ activityProxy.platform_onPointerCaptureChanged(hasCapture)
+ }
+
+ protected open fun onPostCreate(bundle: Bundle?) {
+ activityProxy.platform_onPostCreate(bundle)
+ }
+
+ open fun onPostCreate(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ activityProxy.platform_onPostCreate(bundle, persistableBundle)
+ }
+
+ protected open fun onPostResume() {
+ activityProxy.platform_onPostResume()
+ }
+
+ open fun onPrepareNavigateUpTaskStack(taskStackBuilder: TaskStackBuilder?) {
+ activityProxy.platform_onPrepareNavigateUpTaskStack(taskStackBuilder)
+ }
+
+ open fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ return activityProxy.platform_onPrepareOptionsMenu(menu)
+ }
+
+ open fun onPreparePanel(v: Int, view: View?, menu: Menu): Boolean {
+ return activityProxy.platform_onPreparePanel(v, view, menu)
+ }
+
+ open fun onProvideReferrer(): Uri? {
+ return activityProxy.platform_onProvideReferrer()
+ }
+
+ open fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ activityProxy.platform_onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+
+ open fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray, extra: Int) {
+ activityProxy.platform_onRequestPermissionsResult(requestCode, permissions, grantResults, extra)
+ }
+
+ protected open fun onRestart() {
+ activityProxy.platform_onRestart()
+ }
+
+ protected open fun onRestoreInstanceState(bundle: Bundle) {
+ activityProxy.platform_onRestoreInstanceState(bundle)
+ }
+
+ open fun onRestoreInstanceState(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ activityProxy.platform_onRestoreInstanceState(bundle, persistableBundle)
+ }
+
+ protected open fun onResume() {
+ activityProxy.platform_onResume()
+ }
+
+ protected open fun onSaveInstanceState(bundle: Bundle) {
+ activityProxy.platform_onSaveInstanceState(bundle)
+ }
+
+ open fun onSaveInstanceState(bundle: Bundle, persistableBundle: PersistableBundle) {
+ activityProxy.platform_onSaveInstanceState(bundle, persistableBundle)
+ }
+
+ open fun onSearchRequested(): Boolean {
+ return activityProxy.platform_onSearchRequested()
+ }
+
+ open fun onSearchRequested(searchEvent: SearchEvent?): Boolean {
+ return activityProxy.platform_onSearchRequested(searchEvent)
+ }
+
+ protected open fun onStart() {
+ activityProxy.platform_onStart()
+ }
+
+ @Deprecated
+ open fun onStateNotSaved() {
+ activityProxy.platform_onStateNotSaved()
+ }
+
+ protected open fun onStop() {
+ activityProxy.platform_onStop()
+ }
+
+ protected open fun onTitleChanged(charSequence: CharSequence?, v: Int) {
+ activityProxy.platform_onTitleChanged(charSequence, v)
+ }
+
+ open fun onTouchEvent(motionEvent: MotionEvent?): Boolean {
+ return activityProxy.platform_onTouchEvent(motionEvent)
+ }
+
+ open fun onTrackballEvent(motionEvent: MotionEvent?): Boolean {
+ return activityProxy.platform_onTrackballEvent(motionEvent)
+ }
+
+ open fun onUserInteraction() {
+ activityProxy.platform_onUserInteraction()
+ }
+
+ protected open fun onUserLeaveHint() {
+ activityProxy.platform_onUserLeaveHint()
+ }
+
+ @Deprecated
+ open fun onVisibleBehindCanceled() {
+ activityProxy.platform_onVisibleBehindCanceled()
+ }
+
+ open fun onWindowAttributesChanged(params: WindowManager.LayoutParams?) {
+ activityProxy.platform_onWindowAttributesChanged(params)
+ }
+
+ open fun onWindowFocusChanged(z: Boolean) {
+ activityProxy.platform_onWindowFocusChanged(z)
+ }
+
+ open fun onWindowStartingActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return activityProxy.platform_onWindowStartingActionMode(callback)
+ }
+
+ open fun onWindowStartingActionMode(callback: ActionMode.Callback?, v: Int): ActionMode? {
+ return activityProxy.platform_onWindowStartingActionMode(callback, v)
+ }
+
+ open fun openContextMenu(view: View?) {
+ activityProxy.platform_openContextMenu(view)
+ }
+
+ open fun openOptionsMenu() {
+ activityProxy.platform_openOptionsMenu()
+ }
+
+ open fun overrideActivityTransition(v: Int, v1: Int, v2: Int) {
+ activityProxy.platform_overrideActivityTransition(v, v1, v2)
+ }
+
+ open fun overrideActivityTransition(v: Int, v1: Int, v2: Int, v3: Int) {
+ activityProxy.platform_overrideActivityTransition(v, v1, v2, v3)
+ }
+
+ @Deprecated
+ open fun overridePendingTransition(v: Int, v1: Int) {
+ activityProxy.platform_overridePendingTransition(v, v1)
+ }
+
+ @Deprecated
+ open fun overridePendingTransition(v: Int, v1: Int, v2: Int) {
+ activityProxy.platform_overridePendingTransition(v, v1, v2)
+ }
+
+ open fun postponeEnterTransition() {
+ activityProxy.platform_postponeEnterTransition()
+ }
+
+ open fun public_addContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ this.addContentView(view, params)
+ }
+
+ open fun public_clearOverrideActivityTransition(v: Int) {
+ this.clearOverrideActivityTransition(v)
+ }
+
+ open fun public_closeContextMenu() {
+ this.closeContextMenu()
+ }
+
+ open fun public_closeOptionsMenu() {
+ this.closeOptionsMenu()
+ }
+
+ open fun public_convertFromTranslucent() {
+ this.convertFromTranslucent()
+ }
+
+ open fun public_convertToTranslucent(listener: Any?, activityOptions: ActivityOptions?): Boolean {
+ return this.convertToTranslucent(listener, activityOptions)
+ }
+
+ open fun public_createPendingResult(requestCode: Int, data: Intent, flags: Int): PendingIntent {
+ return this.createPendingResult(requestCode, data, flags)
+ }
+
+ open fun public_dispatchGenericMotionEvent(motionEvent: MotionEvent?): Boolean {
+ return this.dispatchGenericMotionEvent(motionEvent)
+ }
+
+ open fun public_dispatchKeyEvent(keyEvent: KeyEvent?): Boolean {
+ return this.dispatchKeyEvent(keyEvent)
+ }
+
+ open fun public_dispatchKeyShortcutEvent(keyEvent: KeyEvent?): Boolean {
+ return this.dispatchKeyShortcutEvent(keyEvent)
+ }
+
+ open fun public_dispatchPopulateAccessibilityEvent(accessibilityEvent: AccessibilityEvent?): Boolean {
+ return this.dispatchPopulateAccessibilityEvent(accessibilityEvent)
+ }
+
+ open fun public_dispatchTouchEvent(motionEvent: MotionEvent?): Boolean {
+ return this.dispatchTouchEvent(motionEvent)
+ }
+
+ open fun public_dispatchTrackballEvent(motionEvent: MotionEvent?): Boolean {
+ return this.dispatchTrackballEvent(motionEvent)
+ }
+
+ open fun public_finish() {
+ this.finish()
+ }
+
+ open fun public_finishActivity(v: Int) {
+ this.finishActivity(v)
+ }
+
+ @Deprecated
+ open fun public_finishActivityFromChild(activity: Activity, requestCode: Int) {
+ this.finishActivityFromChild(activity, requestCode)
+ }
+
+ open fun public_finishAffinity() {
+ this.finishAffinity()
+ }
+
+ open fun public_finishAfterTransition() {
+ this.finishAfterTransition()
+ }
+
+ open fun public_finishAndRemoveTask() {
+ this.finishAndRemoveTask()
+ }
+
+ @Deprecated
+ open fun public_finishFromChild(activity: Activity?) {
+ this.finishFromChild(activity)
+ }
+
+ open fun public_getActionBar(): ActionBar? {
+ return getActionBar()
+ }
+
+ open fun public_getApplication(): Application? {
+ return getApplication()
+ }
+
+ open fun public_getCaller(): ComponentCaller? {
+ return getCaller()
+ }
+
+ open fun public_getCallingActivity(): ComponentName? {
+ return getCallingActivity()
+ }
+
+ open fun public_getCallingPackage(): String? {
+ return getCallingPackage()
+ }
+
+ open fun public_getChangingConfigurations(): Int {
+ return getChangingConfigurations()
+ }
+
+ open fun public_getComponentName(): ComponentName? {
+ return getComponentName()
+ }
+
+ open fun public_getContentScene(): Scene? {
+ return getContentScene()
+ }
+
+ open fun public_getContentTransitionManager(): TransitionManager? {
+ return getContentTransitionManager()
+ }
+
+ open fun public_getCurrentCaller(): ComponentCaller {
+ return getCurrentCaller()
+ }
+
+ open fun public_getCurrentFocus(): View? {
+ return getCurrentFocus()
+ }
+
+ @Deprecated
+ open fun public_getFragmentManager(): FragmentManager? {
+ return getFragmentManager()
+ }
+
+ open fun public_getInitialCaller(): ComponentCaller {
+ return getInitialCaller()
+ }
+
+ open fun public_getIntent(): Intent? {
+ return getIntent()
+ }
+
+ open fun public_getLastNonConfigurationInstance(): Any? {
+ return getLastNonConfigurationInstance()
+ }
+ open fun public_getLaunchedFromPackage(): String? {
+ return getLaunchedFromPackage()
+ }
+
+ open fun public_getLaunchedFromUid(): Int {
+ return getLaunchedFromUid()
+ }
+
+ open fun public_getLayoutInflater(): LayoutInflater {
+ return getLayoutInflater()
+ }
+
+ @Deprecated
+ open fun public_getLoaderManager(): LoaderManager? {
+ return getLoaderManager()
+ }
+
+ open fun public_getLocalClassName(): String {
+ return getLocalClassName()
+ }
+
+ open fun public_getMaxNumPictureInPictureActions(): Int {
+ return getMaxNumPictureInPictureActions()
+ }
+
+ open fun public_getMediaController(): MediaController? {
+ return getMediaController()
+ }
+
+ open fun public_getMenuInflater(): MenuInflater? {
+ return getMenuInflater()
+ }
+
+ open fun public_getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+ return getOnBackInvokedDispatcher()
+ }
+
+ @Deprecated
+ open fun public_getParent(): Activity? {
+ return getParent()
+ }
+
+ open fun public_getParentActivityIntent(): Intent? {
+ return getParentActivityIntent()
+ }
+
+ open fun public_getPreferences(v: Int): SharedPreferences? {
+ return getPreferences(v)
+ }
+
+ open fun public_getReferrer(): Uri? {
+ return getReferrer()
+ }
+
+ open fun public_getRequestedOrientation(): Int {
+ return getRequestedOrientation()
+ }
+
+ open fun public_getSearchEvent(): SearchEvent? {
+ return getSearchEvent()
+ }
+
+ open fun public_getSplashScreen(): SplashScreen? {
+ return getSplashScreen()
+ }
+
+ open fun public_getTaskId(): Int {
+ return getTaskId()
+ }
+
+ open fun public_getTitle(): CharSequence? {
+ return getTitle()
+ }
+
+ @Deprecated
+ open fun public_getTitleColor(): Int {
+ return getTitleColor()
+ }
+
+ open fun public_getVoiceInteractor(): VoiceInteractor? {
+ return getVoiceInteractor()
+ }
+
+ open fun public_getVolumeControlStream(): Int {
+ return getVolumeControlStream()
+ }
+
+ open fun public_getWindow(): Window? {
+ return getWindow()
+ }
+
+ open fun public_getWindowManager(): WindowManager? {
+ return getWindowManager()
+ }
+
+ open fun public_hasWindowFocus(): Boolean {
+ return this.hasWindowFocus()
+ }
+
+ open fun public_invalidateOptionsMenu() {
+ this.invalidateOptionsMenu()
+ }
+
+ open fun public_isActivityTransitionRunning(): Boolean {
+ return isActivityTransitionRunning()
+ }
+
+ @Deprecated
+ open fun public_isBackgroundVisibleBehind(): Boolean {
+ return isBackgroundVisibleBehind()
+ }
+
+ open fun public_isChangingConfigurations(): Boolean {
+ return this.isChangingConfigurations()
+ }
+ @Deprecated
+ open fun public_isChild(): Boolean {
+ return isChild()
+ }
+
+ open fun public_isDestroyed(): Boolean {
+ return isDestroyed()
+ }
+
+ open fun public_isFinishing(): Boolean {
+ return isFinishing()
+ }
+
+ open fun public_isImmersive(): Boolean {
+ return isImmersive()
+ }
+
+ open fun public_isInMultiWindowMode(): Boolean {
+ return isInMultiWindowMode()
+ }
+
+ open fun public_isInPictureInPictureMode(): Boolean {
+ return isInPictureInPictureMode()
+ }
+
+ open fun public_isLaunchedFromBubble(): Boolean {
+ return isLaunchedFromBubble()
+ }
+
+ open fun public_isLocalVoiceInteractionSupported(): Boolean {
+ return isLocalVoiceInteractionSupported()
+ }
+
+ open fun public_isTaskRoot(): Boolean {
+ return isTaskRoot()
+ }
+
+ open fun public_isVoiceInteraction(): Boolean {
+ return isVoiceInteraction()
+ }
+
+ open fun public_isVoiceInteractionRoot(): Boolean {
+ return isVoiceInteractionRoot()
+ }
+
+ @Deprecated
+ open fun public_managedQuery(
+ uri: Uri?,
+ arr_s: Array?,
+ s: String?,
+ arr_s1: Array?,
+ s1: String?
+ ): Cursor? {
+ return this.managedQuery(uri, arr_s, s, arr_s1, s1)
+ }
+
+ open fun public_moveTaskToBack(z: Boolean): Boolean {
+ return this.moveTaskToBack(z)
+ }
+
+ open fun public_navigateUpTo(intent: Intent?): Boolean {
+ return this.navigateUpTo(intent)
+ }
+
+ @Deprecated
+ open fun public_navigateUpToFromChild(activity: Activity?, intent: Intent?): Boolean {
+ return this.navigateUpToFromChild(activity, intent)
+ }
+
+ open fun public_onActionModeFinished(actionMode: ActionMode?) {
+ this.onActionModeFinished(actionMode)
+ }
+
+ open fun public_onActionModeStarted(actionMode: ActionMode?) {
+ this.onActionModeStarted(actionMode)
+ }
+
+ open fun public_onActivityReenter(v: Int, intent: Intent?) {
+ this.onActivityReenter(v, intent)
+ }
+
+ open fun public_onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ this.onActivityResult(requestCode, resultCode, data)
+ }
+
+ open fun public_onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, caller: ComponentCaller) {
+ this.onActivityResult(requestCode, resultCode, data, caller)
+ }
+
+ @Deprecated
+ open fun public_onAttachFragment(fragment: Fragment?) {
+ this.onAttachFragment(fragment)
+ }
+
+ open fun public_onAttachedToWindow() {
+ this.onAttachedToWindow()
+ }
+
+ @Deprecated
+ open fun public_onBackPressed() {
+ this.onBackPressed()
+ }
+
+ @Deprecated
+ open fun public_onBackgroundVisibleBehindChanged(z: Boolean) {
+ this.onBackgroundVisibleBehindChanged(z)
+ }
+
+ open fun public_onChildTitleChanged(activity: Activity?, charSequence: CharSequence?) {
+ this.onChildTitleChanged(activity, charSequence)
+ }
+
+ open fun public_onConfigurationChanged(configuration: Configuration) {
+ this.onConfigurationChanged(configuration)
+ }
+
+ open fun public_onContentChanged() {
+ this.onContentChanged()
+ }
+
+ open fun public_onContextItemSelected(menuItem: MenuItem): Boolean {
+ return this.onContextItemSelected(menuItem)
+ }
+
+ open fun public_onContextMenuClosed(menu: Menu) {
+ this.onContextMenuClosed(menu)
+ }
+
+ open fun public_onCreate(bundle: Bundle?) {
+ this.onCreate(bundle)
+ }
+
+ open fun public_onCreate(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ this.onCreate(bundle, persistableBundle)
+ }
+
+ open fun public_onCreateContextMenu(
+ contextMenu: ContextMenu?,
+ view: View?,
+ contextMenuInfo: ContextMenu.ContextMenuInfo?
+ ) {
+ this.onCreateContextMenu(contextMenu, view, contextMenuInfo)
+ }
+
+ open fun public_onCreateDescription(): CharSequence? {
+ return this.onCreateDescription()
+ }
+
+ open fun public_onCreateNavigateUpTaskStack(taskStackBuilder: TaskStackBuilder?) {
+ this.onCreateNavigateUpTaskStack(taskStackBuilder)
+ }
+
+ open fun public_onCreateOptionsMenu(menu: Menu): Boolean {
+ return this.onCreateOptionsMenu(menu)
+ }
+
+ open fun public_onCreatePanelMenu(v: Int, menu: Menu): Boolean {
+ return this.onCreatePanelMenu(v, menu)
+ }
+
+ open fun public_onCreatePanelView(v: Int): View? {
+ return this.onCreatePanelView(v)
+ }
+
+ @Deprecated
+ open fun public_onCreateThumbnail(bitmap: Bitmap?, canvas: Canvas?): Boolean {
+ return this.onCreateThumbnail(bitmap, canvas)
+ }
+
+ open fun public_onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
+ return this.onCreateView(parent, name, context, attrs)
+ }
+
+ open fun public_onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ return this.onCreateView(name, context, attrs)
+ }
+
+ open fun public_onDestroy() {
+ this.onDestroy()
+ }
+
+ open fun public_onDetachedFromWindow() {
+ this.onDetachedFromWindow()
+ }
+
+ open fun public_onGenericMotionEvent(motionEvent: MotionEvent?): Boolean {
+ return this.onGenericMotionEvent(motionEvent)
+ }
+
+ open fun public_onKeyDown(v: Int, keyEvent: KeyEvent?): Boolean {
+ return this.onKeyDown(v, keyEvent)
+ }
+
+ open fun public_onKeyLongPress(v: Int, keyEvent: KeyEvent?): Boolean {
+ return this.onKeyLongPress(v, keyEvent)
+ }
+
+ open fun public_onKeyMultiple(v: Int, v1: Int, keyEvent: KeyEvent?): Boolean {
+ return this.onKeyMultiple(v, v1, keyEvent)
+ }
+
+ open fun public_onKeyShortcut(v: Int, keyEvent: KeyEvent?): Boolean {
+ return this.onKeyShortcut(v, keyEvent)
+ }
+
+ open fun public_onKeyUp(v: Int, keyEvent: KeyEvent?): Boolean {
+ return this.onKeyUp(v, keyEvent)
+ }
+
+ open fun public_onLocalVoiceInteractionStarted() {
+ this.onLocalVoiceInteractionStarted()
+ }
+
+ open fun public_onLocalVoiceInteractionStopped() {
+ this.onLocalVoiceInteractionStopped()
+ }
+
+ open fun public_onMenuItemSelected(v: Int, menuItem: MenuItem): Boolean {
+ return this.onMenuItemSelected(v, menuItem)
+ }
+
+ open fun public_onMenuOpened(v: Int, menu: Menu): Boolean {
+ return this.onMenuOpened(v, menu)
+ }
+
+ open fun public_onNavigateUp(): Boolean {
+ return this.onNavigateUp()
+ }
+
+ @Deprecated
+ open fun public_onNavigateUpFromChild(activity: Activity?): Boolean {
+ return this.onNavigateUpFromChild(activity)
+ }
+
+ open fun public_onNewIntent(intent: Intent?) {
+ this.onNewIntent(intent)
+ }
+
+ open fun public_onNewIntent(intent: Intent, componentCaller: ComponentCaller) {
+ this.onNewIntent(intent, componentCaller)
+ }
+
+ open fun public_onOptionsItemSelected(menuItem: MenuItem): Boolean {
+ return this.onOptionsItemSelected(menuItem)
+ }
+
+ open fun public_onOptionsMenuClosed(menu: Menu) {
+ this.onOptionsMenuClosed(menu)
+ }
+
+ open fun public_onPanelClosed(v: Int, menu: Menu) {
+ this.onPanelClosed(v, menu)
+ }
+
+ open fun public_onPause() {
+ this.onPause()
+ }
+
+ open fun public_onPointerCaptureChanged(z: Boolean) {
+ this.onPointerCaptureChanged(z)
+ }
+
+ open fun public_onPostCreate(bundle: Bundle?) {
+ this.onPostCreate(bundle)
+ }
+
+ open fun public_onPostCreate(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ this.onPostCreate(bundle, persistableBundle)
+ }
+
+ open fun public_onPostResume() {
+ this.onPostResume()
+ }
+
+ open fun public_onPrepareNavigateUpTaskStack(taskStackBuilder: TaskStackBuilder?) {
+ this.onPrepareNavigateUpTaskStack(taskStackBuilder)
+ }
+
+ open fun public_onPrepareOptionsMenu(menu: Menu): Boolean {
+ return this.onPrepareOptionsMenu(menu)
+ }
+
+ open fun public_onPreparePanel(v: Int, view: View?, menu: Menu): Boolean {
+ return this.onPreparePanel(v, view, menu)
+ }
+
+ open fun public_onProvideReferrer(): Uri? {
+ return this.onProvideReferrer()
+ }
+
+ open fun public_onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ this.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+
+ open fun public_onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray, extra: Int) {
+ this.onRequestPermissionsResult(requestCode, permissions, grantResults, extra)
+ }
+
+ open fun public_onRestart() {
+ this.onRestart()
+ }
+
+ open fun public_onRestoreInstanceState(bundle: Bundle) {
+ this.onRestoreInstanceState(bundle)
+ }
+
+ open fun public_onRestoreInstanceState(bundle: Bundle?, persistableBundle: PersistableBundle?) {
+ this.onRestoreInstanceState(bundle, persistableBundle)
+ }
+
+ open fun public_onResume() {
+ this.onResume()
+ }
+
+ open fun public_onSaveInstanceState(bundle: Bundle) {
+ this.onSaveInstanceState(bundle)
+ }
+
+ open fun public_onSaveInstanceState(bundle: Bundle, persistableBundle: PersistableBundle) {
+ this.onSaveInstanceState(bundle, persistableBundle)
+ }
+
+ open fun public_onSearchRequested(): Boolean {
+ return this.onSearchRequested()
+ }
+
+ open fun public_onSearchRequested(searchEvent: SearchEvent?): Boolean {
+ return this.onSearchRequested(searchEvent)
+ }
+
+ open fun public_onStart() {
+ this.onStart()
+ }
+
+ @Deprecated
+ open fun public_onStateNotSaved() {
+ this.onStateNotSaved()
+ }
+
+ open fun public_onStop() {
+ this.onStop()
+ }
+
+ open fun public_onTitleChanged(charSequence: CharSequence?, v: Int) {
+ this.onTitleChanged(charSequence, v)
+ }
+
+ open fun public_onTouchEvent(motionEvent: MotionEvent?): Boolean {
+ return this.onTouchEvent(motionEvent)
+ }
+
+ open fun public_onTrackballEvent(motionEvent: MotionEvent?): Boolean {
+ return this.onTrackballEvent(motionEvent)
+ }
+
+ open fun public_onUserInteraction() {
+ this.onUserInteraction()
+ }
+
+ open fun public_onUserLeaveHint() {
+ this.onUserLeaveHint()
+ }
+
+ @Deprecated
+ open fun public_onVisibleBehindCanceled() {
+ this.onVisibleBehindCanceled()
+ }
+
+ open fun public_onWindowAttributesChanged(params: WindowManager.LayoutParams?) {
+ this.onWindowAttributesChanged(params)
+ }
+
+ open fun public_onWindowFocusChanged(z: Boolean) {
+ this.onWindowFocusChanged(z)
+ }
+
+ open fun public_onWindowStartingActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return this.onWindowStartingActionMode(callback)
+ }
+
+ open fun public_onWindowStartingActionMode(
+ callback: ActionMode.Callback?,
+ v: Int
+ ): ActionMode? {
+ return this.onWindowStartingActionMode(callback, v)
+ }
+
+ open fun public_openContextMenu(view: View?) {
+ this.openContextMenu(view)
+ }
+
+ open fun public_openOptionsMenu() {
+ this.openOptionsMenu()
+ }
+
+ open fun public_overrideActivityTransition(v: Int, v1: Int, v2: Int) {
+ this.overrideActivityTransition(v, v1, v2)
+ }
+
+ open fun public_overrideActivityTransition(v: Int, v1: Int, v2: Int, v3: Int) {
+ this.overrideActivityTransition(v, v1, v2, v3)
+ }
+
+ @Deprecated
+ open fun public_overridePendingTransition(v: Int, v1: Int) {
+ this.overridePendingTransition(v, v1)
+ }
+
+ @Deprecated
+ open fun public_overridePendingTransition(v: Int, v1: Int, v2: Int) {
+ this.overridePendingTransition(v, v1, v2)
+ }
+
+ open fun public_postponeEnterTransition() {
+ this.postponeEnterTransition()
+ }
+
+ open fun public_recreate() {
+ this.recreate()
+ }
+
+ open fun public_registerActivityLifecycleCallbacks(callback: Application.ActivityLifecycleCallbacks) {
+ this.registerActivityLifecycleCallbacks(callback)
+ }
+
+ open fun public_registerForContextMenu(view: View?) {
+ this.registerForContextMenu(view)
+ }
+
+ open fun public_releaseInstance(): Boolean {
+ return this.releaseInstance()
+ }
+
+ open fun public_reportFullyDrawn() {
+ this.reportFullyDrawn()
+ }
+
+ open fun public_requestDragAndDropPermissions(dragEvent: DragEvent?): DragAndDropPermissions? {
+ return this.requestDragAndDropPermissions(dragEvent)
+ }
+
+ open fun public_requestFullscreenMode(request: Int, outcomeReceiver: OutcomeReceiver?) {
+ this.requestFullscreenMode(request, outcomeReceiver)
+ }
+
+ open fun public_requestPermissions(permissions: Array, requestCode: Int) {
+ this.requestPermissions(permissions, requestCode)
+ }
+
+ open fun public_requestPermissions(permissions: Array, requestCode: Int, userId: Int) {
+ this.requestPermissions(permissions, requestCode, userId)
+ }
+
+ @Deprecated
+ open fun public_requestVisibleBehind(z: Boolean): Boolean {
+ return this.requestVisibleBehind(z)
+ }
+
+ open fun public_requestWindowFeature(v: Int): Boolean {
+ return this.requestWindowFeature(v)
+ }
+
+ open fun public_requireViewById(v: Int): View? {
+ return this.requireViewById(v)
+ }
+
+ open fun public_runOnUiThread(runnable: Runnable?) {
+ this.runOnUiThread(runnable)
+ }
+
+ open fun public_setActionBar(toolbar: Toolbar?) {
+ this.setActionBar(toolbar)
+ }
+
+ open fun public_setAllowCrossUidActivitySwitchFromBelow(z: Boolean) {
+ this.setAllowCrossUidActivitySwitchFromBelow(z)
+ }
+
+ open fun public_setContentTransitionManager(transitionManager: TransitionManager?) {
+ this.setContentTransitionManager(transitionManager)
+ }
+
+ open fun public_setContentView(v: Int) {
+ this.setContentView(v)
+ }
+
+ open fun public_setContentView(view: View?) {
+ this.setContentView(view)
+ }
+
+ open fun public_setContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ this.setContentView(view, params)
+ }
+
+ open fun public_setDefaultKeyMode(v: Int) {
+ this.setDefaultKeyMode(v)
+ }
+
+ open fun public_setEnterSharedElementCallback(sharedElementCallback: SharedElementCallback?) {
+ this.setEnterSharedElementCallback(sharedElementCallback)
+ }
+
+ open fun public_setExitSharedElementCallback(sharedElementCallback: SharedElementCallback?) {
+ this.setExitSharedElementCallback(sharedElementCallback)
+ }
+
+ open fun public_setFeatureDrawable(v: Int, drawable: Drawable?) {
+ this.setFeatureDrawable(v, drawable)
+ }
+
+ open fun public_setFeatureDrawableAlpha(v: Int, v1: Int) {
+ this.setFeatureDrawableAlpha(v, v1)
+ }
+
+ open fun public_setFeatureDrawableResource(v: Int, v1: Int) {
+ this.setFeatureDrawableResource(v, v1)
+ }
+
+ open fun public_setFeatureDrawableUri(v: Int, uri: Uri?) {
+ this.setFeatureDrawableUri(v, uri)
+ }
+
+ open fun public_setFinishOnTouchOutside(z: Boolean) {
+ this.setFinishOnTouchOutside(z)
+ }
+
+ open fun public_setImmersive(z: Boolean) {
+ setImmersive(z)
+ }
+
+ open fun public_setInheritShowWhenLocked(z: Boolean) {
+ this.setInheritShowWhenLocked(z)
+ }
+
+ open fun public_setIntent(intent: Intent?) {
+ this.setIntent(intent)
+ }
+
+ open fun public_setIntent(intent: Intent?, componentCaller: ComponentCaller?) {
+ this.setIntent(intent, componentCaller)
+ }
+
+ open fun public_setLocusContext(locusId: LocusId?, bundle: Bundle?) {
+ this.setLocusContext(locusId, bundle)
+ }
+
+ open fun public_setMediaController(mediaController: MediaController?) {
+ setMediaController(mediaController)
+ }
+
+ open fun public_setPictureInPictureParams(pictureInPictureParams: PictureInPictureParams) {
+ this.setPictureInPictureParams(pictureInPictureParams)
+ }
+
+ @Deprecated
+ open fun public_setProgress(v: Int) {
+ this.setProgress(v)
+ }
+
+ @Deprecated
+ open fun public_setProgressBarIndeterminate(z: Boolean) {
+ this.setProgressBarIndeterminate(z)
+ }
+
+ @Deprecated
+ open fun public_setProgressBarIndeterminateVisibility(z: Boolean) {
+ this.setProgressBarIndeterminateVisibility(z)
+ }
+
+ @Deprecated
+ open fun public_setProgressBarVisibility(z: Boolean) {
+ this.setProgressBarVisibility(z)
+ }
+
+ open fun public_setRecentsScreenshotEnabled(z: Boolean) {
+ this.setRecentsScreenshotEnabled(z)
+ }
+
+ open fun public_setRequestedOrientation(v: Int) {
+ setRequestedOrientation(v)
+ }
+
+ open fun public_setResult(v: Int) {
+ this.setResult(v)
+ }
+
+ open fun public_setResult(v: Int, intent: Intent?) {
+ this.setResult(v, intent)
+ }
+
+ @Deprecated
+ open fun public_setSecondaryProgress(v: Int) {
+ this.setSecondaryProgress(v)
+ }
+
+ open fun public_setShouldDockBigOverlays(z: Boolean) {
+ this.setShouldDockBigOverlays(z)
+ }
+
+ open fun public_setShowWhenLocked(z: Boolean) {
+ this.setShowWhenLocked(z)
+ }
+
+ open fun public_setTaskDescription(taskDescription: ActivityManager.TaskDescription?) {
+ this.setTaskDescription(taskDescription)
+ }
+
+ open fun public_setTitle(v: Int) {
+ this.setTitle(v)
+ }
+
+ open fun public_setTitle(charSequence: CharSequence?) {
+ this.setTitle(charSequence)
+ }
+
+ @Deprecated
+ open fun public_setTitleColor(v: Int) {
+ setTitleColor(v)
+ }
+
+ open fun public_setTranslucent(z: Boolean): Boolean {
+ return this.setTranslucent(z)
+ }
+
+ open fun public_setTurnScreenOn(z: Boolean) {
+ this.setTurnScreenOn(z)
+ }
+
+ open fun public_setVisible(z: Boolean) {
+ this.setVisible(z)
+ }
+
+ open fun public_setVolumeControlStream(v: Int) {
+ setVolumeControlStream(v)
+ }
+
+ open fun public_setVrModeEnabled(enabled: Boolean, componentName: ComponentName) {
+ this.setVrModeEnabled(enabled, componentName)
+ }
+
+ open fun public_shouldDockBigOverlays(): Boolean {
+ return this.shouldDockBigOverlays()
+ }
+
+ open fun public_shouldShowRequestPermissionRationale(permission: String): Boolean {
+ return this.shouldShowRequestPermissionRationale(permission)
+ }
+
+ open fun public_shouldShowRequestPermissionRationale(permission: String, userId: Int): Boolean {
+ return this.shouldShowRequestPermissionRationale(permission, userId)
+ }
+
+ open fun public_shouldUpRecreateTask(intent: Intent?): Boolean {
+ return this.shouldUpRecreateTask(intent)
+ }
+
+ open fun public_showAssist(bundle: Bundle?): Boolean {
+ return this.showAssist(bundle)
+ }
+
+ open fun public_showLockTaskEscapeMessage() {
+ this.showLockTaskEscapeMessage()
+ }
+
+ open fun public_startActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return this.startActionMode(callback)
+ }
+
+ open fun public_startActionMode(callback: ActionMode.Callback?, v: Int): ActionMode? {
+ return this.startActionMode(callback, v)
+ }
+
+ open fun public_startActivities(arr_intent: Array) {
+ this.startActivities(arr_intent)
+ }
+
+ open fun public_startActivities(arr_intent: Array, bundle: Bundle?) {
+ this.startActivities(arr_intent, bundle)
+ }
+
+ open fun public_startActivity(intent: Intent?) {
+ this.startActivity(intent)
+ }
+
+ open fun public_startActivity(intent: Intent?, bundle: Bundle?) {
+ this.startActivity(intent, bundle)
+ }
+
+ open fun public_startActivityForResult(intent: Intent?, v: Int) {
+ this.startActivityForResult(intent, v)
+ }
+
+ open fun public_startActivityForResult(intent: Intent?, v: Int, bundle: Bundle?) {
+ this.startActivityForResult(intent, v, bundle)
+ }
+
+ open fun public_startActivityForResultAsUser(
+ intent: Intent?,
+ v: Int,
+ bundle: Bundle?,
+ userHandle: UserHandle?
+ ) {
+ this.startActivityForResultAsUser(intent, v, bundle, userHandle)
+ }
+
+ open fun public_startActivityForResultAsUser(intent: Intent?, v: Int, userHandle: UserHandle?) {
+ this.startActivityForResultAsUser(intent, v, userHandle)
+ }
+
+ open fun public_startActivityForResultAsUser(
+ intent: Intent?,
+ s: String?,
+ v: Int,
+ bundle: Bundle?,
+ userHandle: UserHandle?
+ ) {
+ this.startActivityForResultAsUser(intent, s, v, bundle, userHandle)
+ }
+
+ @Deprecated
+ open fun public_startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int) {
+ this.startActivityFromChild(child, intent, requestCode)
+ }
+
+ @Deprecated
+ open fun public_startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int, options: Bundle?) {
+ this.startActivityFromChild(child, intent, requestCode, options)
+ }
+
+ @Deprecated
+ open fun public_startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int) {
+ this.startActivityFromFragment(fragment, intent, requestCode)
+ }
+
+ @Deprecated
+ open fun public_startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int, options: Bundle?) {
+ this.startActivityFromFragment(fragment, intent, requestCode, options)
+ }
+
+ open fun public_startActivityIfNeeded(intent: Intent, requestCode: Int): Boolean {
+ return this.startActivityIfNeeded(intent, requestCode)
+ }
+
+ open fun public_startActivityIfNeeded(intent: Intent, requestCode: Int, options: Bundle?): Boolean {
+ return this.startActivityIfNeeded(intent, requestCode, options)
+ }
+
+ open fun public_startIntentSender(
+ intentSender: IntentSender,
+ intent: Intent?,
+ v: Int,
+ v1: Int,
+ v2: Int
+ ) {
+ this.startIntentSender(intentSender, intent, v, v1, v2)
+ }
+
+ open fun public_startIntentSender(
+ intentSender: IntentSender,
+ intent: Intent?,
+ v: Int,
+ v1: Int,
+ v2: Int,
+ bundle: Bundle?
+ ) {
+ this.startIntentSender(intentSender, intent, v, v1, v2, bundle)
+ }
+
+ open fun public_startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ this.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ open fun public_startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ this.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ @Deprecated
+ open fun public_startIntentSenderFromChild(
+ activity: Activity?,
+ intentSender: IntentSender?,
+ v: Int,
+ intent: Intent?,
+ v1: Int,
+ v2: Int,
+ v3: Int
+ ) {
+ this.startIntentSenderFromChild(activity, intentSender, v, intent, v1, v2, v3)
+ }
+
+ @Deprecated
+ open fun public_startIntentSenderFromChild(
+ activity: Activity?,
+ intentSender: IntentSender?,
+ v: Int,
+ intent: Intent?,
+ v1: Int,
+ v2: Int,
+ v3: Int,
+ bundle: Bundle?
+ ) {
+ this.startIntentSenderFromChild(activity, intentSender, v, intent, v1, v2, v3, bundle)
+ }
+
+ open fun public_startLocalVoiceInteraction(bundle: Bundle?) {
+ this.startLocalVoiceInteraction(bundle)
+ }
+
+ open fun public_startLockTask() {
+ this.startLockTask()
+ }
+
+ @Deprecated
+ open fun public_startManagingCursor(cursor: Cursor?) {
+ this.startManagingCursor(cursor)
+ }
+
+ open fun public_startNextMatchingActivity(intent: Intent): Boolean {
+ return this.startNextMatchingActivity(intent)
+ }
+
+ open fun public_startNextMatchingActivity(intent: Intent, bundle: Bundle?): Boolean {
+ return this.startNextMatchingActivity(intent, bundle)
+ }
+
+ open fun public_startPostponedEnterTransition() {
+ this.startPostponedEnterTransition()
+ }
+
+ open fun public_startSearch(s: String?, z: Boolean, bundle: Bundle?, z1: Boolean) {
+ this.startSearch(s, z, bundle, z1)
+ }
+
+ open fun public_stopLocalVoiceInteraction() {
+ this.stopLocalVoiceInteraction()
+ }
+
+ open fun public_stopLockTask() {
+ this.stopLockTask()
+ }
+
+ @Deprecated
+ open fun public_stopManagingCursor(cursor: Cursor?) {
+ this.stopManagingCursor(cursor)
+ }
+
+ open fun public_takeKeyEvents(z: Boolean) {
+ this.takeKeyEvents(z)
+ }
+
+ open fun public_triggerSearch(s: String?, bundle: Bundle?) {
+ this.triggerSearch(s, bundle)
+ }
+
+ open fun public_unregisterActivityLifecycleCallbacks(callbacks: Application.ActivityLifecycleCallbacks) {
+ this.unregisterActivityLifecycleCallbacks(callbacks)
+ }
+
+ open fun public_unregisterForContextMenu(view: View?) {
+ this.unregisterForContextMenu(view)
+ }
+
+ open fun recreate() {
+ activityProxy.platform_recreate()
+ }
+
+ open fun registerActivityLifecycleCallbacks(callbacks: Application.ActivityLifecycleCallbacks) {
+ activityProxy.platform_registerActivityLifecycleCallbacks(callbacks)
+ }
+
+ open fun registerForContextMenu(view: View?) {
+ activityProxy.platform_registerForContextMenu(view)
+ }
+
+ open fun releaseInstance(): Boolean {
+ return activityProxy.platform_releaseInstance()
+ }
+
+ open fun reportFullyDrawn() {
+ activityProxy.platform_reportFullyDrawn()
+ }
+
+ open fun requestDragAndDropPermissions(dragEvent: DragEvent?): DragAndDropPermissions? {
+ return activityProxy.platform_requestDragAndDropPermissions(dragEvent)
+ }
+
+ open fun requestFullscreenMode(request: Int, approvalCallback: OutcomeReceiver?) {
+ activityProxy.platform_requestFullscreenMode(request, approvalCallback)
+ }
+
+ open fun requestPermissions(permissions: Array, requestCode: Int) {
+ activityProxy.platform_requestPermissions(permissions, requestCode)
+ }
+
+ open fun requestPermissions(permissions: Array, requestCode: Int, userId: Int) {
+ activityProxy.platform_requestPermissions(permissions, requestCode, userId)
+ }
+
+ @Deprecated
+ open fun requestVisibleBehind(z: Boolean): Boolean {
+ return activityProxy.platform_requestVisibleBehind(z)
+ }
+
+ open fun requestWindowFeature(v: Int): Boolean {
+ return activityProxy.platform_requestWindowFeature(v)
+ }
+
+ open fun requireViewById(v: Int): View? {
+ return activityProxy.platform_requireViewById(v)
+ }
+
+ open fun runOnUiThread(runnable: Runnable?) {
+ activityProxy.platform_runOnUiThread(runnable)
+ }
+
+ open fun setActionBar(toolbar: Toolbar?) {
+ activityProxy.platform_setActionBar(toolbar)
+ }
+
+ open fun setAllowCrossUidActivitySwitchFromBelow(z: Boolean) {
+ activityProxy.platform_setAllowCrossUidActivitySwitchFromBelow(z)
+ }
+
+ open fun setContentTransitionManager(transitionManager: TransitionManager?) {
+ this.activityProxy.platform_setContentTransitionManager(transitionManager)
+ }
+
+ open fun setContentView(v: Int) {
+ activityProxy.platform_setContentView(v)
+ }
+
+ open fun setContentView(view: View?) {
+ activityProxy.platform_setContentView(view)
+ }
+
+ open fun setContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ activityProxy.platform_setContentView(view, params)
+ }
+
+ open fun setDefaultKeyMode(v: Int) {
+ activityProxy.platform_setDefaultKeyMode(v)
+ }
+
+ open fun setEnterSharedElementCallback(sharedElementCallback: SharedElementCallback?) {
+ activityProxy.platform_setEnterSharedElementCallback(sharedElementCallback)
+ }
+
+ open fun setExitSharedElementCallback(sharedElementCallback: SharedElementCallback?) {
+ activityProxy.platform_setExitSharedElementCallback(sharedElementCallback)
+ }
+
+ open fun setFeatureDrawable(v: Int, drawable: Drawable?) {
+ activityProxy.platform_setFeatureDrawable(v, drawable)
+ }
+
+ open fun setFeatureDrawableAlpha(v: Int, v1: Int) {
+ activityProxy.platform_setFeatureDrawableAlpha(v, v1)
+ }
+
+ open fun setFeatureDrawableResource(v: Int, v1: Int) {
+ activityProxy.platform_setFeatureDrawableResource(v, v1)
+ }
+
+ open fun setFeatureDrawableUri(v: Int, uri: Uri?) {
+ activityProxy.platform_setFeatureDrawableUri(v, uri)
+ }
+
+ open fun setFinishOnTouchOutside(z: Boolean) {
+ activityProxy.platform_setFinishOnTouchOutside(z)
+ }
+
+ open fun setInheritShowWhenLocked(z: Boolean) {
+ activityProxy.platform_setInheritShowWhenLocked(z)
+ }
+
+ open fun setIntent(intent: Intent?) {
+ this.activityProxy.platform_setIntent(intent)
+ }
+
+ open fun setIntent(intent: Intent?, componentCaller: ComponentCaller?) {
+ activityProxy.platform_setIntent(intent, componentCaller)
+ }
+
+ open fun setLocusContext(locusId: LocusId?, bundle: Bundle?) {
+ activityProxy.platform_setLocusContext(locusId, bundle)
+ }
+
+ open fun setPictureInPictureParams(pictureInPictureParams: PictureInPictureParams) {
+ activityProxy.platform_setPictureInPictureParams(pictureInPictureParams)
+ }
+
+ @Deprecated
+ open fun setProgress(v: Int) {
+ activityProxy.platform_setProgress(v)
+ }
+
+ @Deprecated
+ open fun setProgressBarIndeterminate(z: Boolean) {
+ activityProxy.platform_setProgressBarIndeterminate(z)
+ }
+
+ @Deprecated
+ open fun setProgressBarIndeterminateVisibility(z: Boolean) {
+ activityProxy.platform_setProgressBarIndeterminateVisibility(z)
+ }
+
+ @Deprecated
+ open fun setProgressBarVisibility(z: Boolean) {
+ activityProxy.platform_setProgressBarVisibility(z)
+ }
+
+ open fun setProxyCallbacks(activityProxy: IActivityProxy, context: Context?) {
+ this.activityProxy = activityProxy
+ }
+
+ open fun setRecentsScreenshotEnabled(z: Boolean) {
+ activityProxy.platform_setRecentsScreenshotEnabled(z)
+ }
+
+ open fun setResult(v: Int) {
+ activityProxy.platform_setResult(v)
+ }
+
+ open fun setResult(v: Int, intent: Intent?) {
+ activityProxy.platform_setResult(v, intent)
+ }
+
+ @Deprecated
+ open fun setSecondaryProgress(v: Int) {
+ activityProxy.platform_setSecondaryProgress(v)
+ }
+
+ open fun setShouldDockBigOverlays(z: Boolean) {
+ activityProxy.platform_setShouldDockBigOverlays(z)
+ }
+
+ open fun setShowWhenLocked(z: Boolean) {
+ activityProxy.platform_setShowWhenLocked(z)
+ }
+
+ open fun setTaskDescription(taskDescription: ActivityManager.TaskDescription?) {
+ activityProxy.platform_setTaskDescription(taskDescription)
+ }
+
+ open fun setTitle(v: Int) {
+ activityProxy.platform_setTitle(v)
+ }
+
+ open fun setTitle(charSequence: CharSequence?) {
+ activityProxy.platform_setTitle(charSequence)
+ }
+
+ open fun setTranslucent(z: Boolean): Boolean {
+ return activityProxy.platform_setTranslucent(z)
+ }
+
+ open fun setTurnScreenOn(z: Boolean) {
+ activityProxy.platform_setTurnScreenOn(z)
+ }
+
+ open fun setVisible(z: Boolean) {
+ activityProxy.platform_setVisible(z)
+ }
+
+ open fun setVrModeEnabled(enabled: Boolean, requestedComponent: ComponentName) {
+ activityProxy.platform_setVrModeEnabled(enabled, requestedComponent)
+ }
+
+ open fun shouldDockBigOverlays(): Boolean {
+ return activityProxy.platform_shouldDockBigOverlays()
+ }
+
+ open fun shouldShowRequestPermissionRationale(permission: String): Boolean {
+ return activityProxy.platform_shouldShowRequestPermissionRationale(permission)
+ }
+
+ open fun shouldShowRequestPermissionRationale(permission: String, userId: Int): Boolean {
+ return activityProxy.platform_shouldShowRequestPermissionRationale(permission, userId)
+ }
+
+ open fun shouldUpRecreateTask(intent: Intent?): Boolean {
+ return activityProxy.platform_shouldUpRecreateTask(intent)
+ }
+
+ open fun showAssist(bundle: Bundle?): Boolean {
+ return activityProxy.platform_showAssist(bundle)
+ }
+
+ open fun showLockTaskEscapeMessage() {
+ activityProxy.platform_showLockTaskEscapeMessage()
+ }
+
+ open fun startActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return activityProxy.platform_startActionMode(callback)
+ }
+
+ open fun startActionMode(callback: ActionMode.Callback?, v: Int): ActionMode? {
+ return activityProxy.platform_startActionMode(callback, v)
+ }
+ override open fun startActivities(intents: Array) {
+ activityProxy.platform_startActivities(intents)
+ }
+ override open fun startActivities(intents: Array, options: Bundle?) {
+ activityProxy.platform_startActivities(intents, options)
+ }
+ override open fun startActivity(intent: Intent?) {
+ activityProxy.platform_startActivity(intent)
+ }
+ override open fun startActivity(intent: Intent?, bundle: Bundle?) {
+ activityProxy.platform_startActivity(intent, bundle)
+ }
+
+ open fun startActivityForResult(intent: Intent?, v: Int) {
+ activityProxy.platform_startActivityForResult(intent, v)
+ }
+
+ open fun startActivityForResult(intent: Intent?, v: Int, bundle: Bundle?) {
+ activityProxy.platform_startActivityForResult(intent, v, bundle)
+ }
+
+ open fun startActivityForResultAsUser(
+ intent: Intent?,
+ v: Int,
+ bundle: Bundle?,
+ userHandle: UserHandle?
+ ) {
+ activityProxy.platform_startActivityForResultAsUser(intent, v, bundle, userHandle)
+ }
+
+ open fun startActivityForResultAsUser(intent: Intent?, v: Int, userHandle: UserHandle?) {
+ activityProxy.platform_startActivityForResultAsUser(intent, v, userHandle)
+ }
+
+ open fun startActivityForResultAsUser(
+ intent: Intent?,
+ s: String?,
+ v: Int,
+ bundle: Bundle?,
+ userHandle: UserHandle?
+ ) {
+ activityProxy.platform_startActivityForResultAsUser(intent, s, v, bundle, userHandle)
+ }
+
+ @Deprecated
+ open fun startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int) {
+ activityProxy.platform_startActivityFromChild(child, intent, requestCode)
+ }
+
+ @Deprecated
+ open fun startActivityFromChild(activity: Activity, intent: Intent?, v: Int, bundle: Bundle?) {
+ activityProxy.platform_startActivityFromChild(activity, intent, v, bundle)
+ }
+
+ @Deprecated
+ open fun startActivityFromFragment(fragment: Fragment, intent: Intent?, v: Int) {
+ activityProxy.platform_startActivityFromFragment(fragment, intent, v)
+ }
+
+ @Deprecated
+ open fun startActivityFromFragment(fragment: Fragment, intent: Intent?, v: Int, bundle: Bundle?) {
+ activityProxy.platform_startActivityFromFragment(fragment, intent, v, bundle)
+ }
+
+ open fun startActivityIfNeeded(intent: Intent, requestCode: Int): Boolean {
+ return activityProxy.platform_startActivityIfNeeded(intent, requestCode)
+ }
+
+ open fun startActivityIfNeeded(intent: Intent, requestCode: Int, options: Bundle?): Boolean {
+ return activityProxy.platform_startActivityIfNeeded(intent, requestCode, options)
+ }
+ override fun startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ activityProxy.platform_startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ override fun startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ activityProxy.platform_startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ open fun startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ activityProxy.platform_startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ open fun startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ activityProxy.platform_startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ @Deprecated
+ open fun startIntentSenderFromChild(
+ activity: Activity?,
+ intentSender: IntentSender?,
+ v: Int,
+ intent: Intent?,
+ v1: Int,
+ v2: Int,
+ v3: Int
+ ) {
+ activityProxy.platform_startIntentSenderFromChild(
+ activity,
+ intentSender,
+ v,
+ intent,
+ v1,
+ v2,
+ v3
+ )
+ }
+
+ @Deprecated
+ open fun startIntentSenderFromChild(
+ activity: Activity?,
+ intentSender: IntentSender?,
+ v: Int,
+ intent: Intent?,
+ v1: Int,
+ v2: Int,
+ v3: Int,
+ bundle: Bundle?
+ ) {
+ activityProxy.platform_startIntentSenderFromChild(
+ activity,
+ intentSender,
+ v,
+ intent,
+ v1,
+ v2,
+ v3,
+ bundle
+ )
+ }
+
+ open fun startLocalVoiceInteraction(bundle: Bundle?) {
+ activityProxy.platform_startLocalVoiceInteraction(bundle)
+ }
+
+ open fun startLockTask() {
+ activityProxy.platform_startLockTask()
+ }
+
+ @Deprecated
+ open fun startManagingCursor(cursor: Cursor?) {
+ activityProxy.platform_startManagingCursor(cursor)
+ }
+
+ open fun startNextMatchingActivity(intent: Intent): Boolean {
+ return activityProxy.platform_startNextMatchingActivity(intent)
+ }
+
+ open fun startNextMatchingActivity(intent: Intent, bundle: Bundle?): Boolean {
+ return activityProxy.platform_startNextMatchingActivity(intent, bundle)
+ }
+
+ open fun startPostponedEnterTransition() {
+ activityProxy.platform_startPostponedEnterTransition()
+ }
+
+ open fun startSearch(s: String?, z: Boolean, bundle: Bundle?, z1: Boolean) {
+ activityProxy.platform_startSearch(s, z, bundle, z1)
+ }
+
+ open fun stopLocalVoiceInteraction() {
+ activityProxy.platform_stopLocalVoiceInteraction()
+ }
+
+ open fun stopLockTask() {
+ activityProxy.platform_stopLockTask()
+ }
+
+ @Deprecated
+ open fun stopManagingCursor(cursor: Cursor?) {
+ activityProxy.platform_stopManagingCursor(cursor)
+ }
+
+ open fun takeKeyEvents(z: Boolean) {
+ activityProxy.platform_takeKeyEvents(z)
+ }
+
+ open fun triggerSearch(s: String?, bundle: Bundle?) {
+ activityProxy.platform_triggerSearch(s, bundle)
+ }
+
+ open fun unregisterActivityLifecycleCallbacks(callbacks: Application.ActivityLifecycleCallbacks) {
+ activityProxy.platform_unregisterActivityLifecycleCallbacks(callbacks)
+ }
+
+ open fun unregisterForContextMenu(view: View?) {
+ activityProxy.platform_unregisterForContextMenu(view)
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/android/ChimeraActivityProxy.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/ChimeraActivityProxy.kt
new file mode 100644
index 0000000000..99933a54e0
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/ChimeraActivityProxy.kt
@@ -0,0 +1,524 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.android
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.res.AssetManager
+import android.content.res.Resources
+import android.net.Uri
+import android.os.Bundle
+import android.os.StrictMode
+import android.util.AttributeSet
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import com.google.android.chimera.InstanceProvider
+import com.google.android.chimera.component.BaseActivityProxy
+import com.google.android.chimera.component.ChimeraComponentProxy
+import com.google.android.chimera.component.ChimeraFallbackActImpl
+import com.google.android.chimera.component.ChimeraModuleContextProvider
+import com.google.android.chimera.component.ChimeraPermissionActImpl
+import com.google.android.chimera.component.ChimeraProxyCallback
+import com.google.android.chimera.component.ContainerApk
+import com.google.android.chimera.config.ChimeraApkManifestReader
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.ChimeraModuleBootstrap
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.config.ModuleDownloadRegistry
+import com.google.android.chimera.context.GmsContextWrapper
+import com.google.android.chimera.context.ModuleContext
+import com.google.android.chimera.loader.ChimeraModuleLdr
+import com.google.android.chimera.util.ChimeraResource
+import com.google.android.chimera.util.ChimeraViewCreator
+import java.lang.reflect.Constructor
+import java.lang.reflect.Method
+
+open class ChimeraActivityProxy : BaseActivityProxy(), IChimeraActivityProxy, ChimeraModuleContextProvider {
+ companion object {
+ private const val TAG = "ChimeraActivityProxy"
+ private const val REQUEST_MODULE_PERMISSION = 0x4348
+ private const val STATE_MODULE = "_chimera_module_state"
+ private const val STATE_FEATURE_REQUEST = "_chimera_attempt_ftr_req"
+ }
+
+ var hasFeatureRequest = false
+ private var isAttachingBaseContext = false
+ private var permissionRedirectLaunched = false
+ private var activityImpl: Activity? = null
+ private var layoutInflater: LayoutInflater? = null
+ private lateinit var containerClassLoader: ClassLoader
+ private lateinit var moduleClassLoader: ClassLoader
+ private lateinit var currentClassLoader: ClassLoader
+
+ private enum class DynamicComponentResolution {
+ LOADED,
+ PERMISSION_REQUIRED,
+ UNAVAILABLE
+ }
+
+ private fun resolveDynamicComponent(): DynamicComponentResolution {
+ val currentClassName = javaClass.name
+ if (!DynamicModuleSettings.isAvailable(this)) {
+ Log.d(TAG, "Dynamic modules unavailable for $currentClassName")
+ return DynamicComponentResolution.UNAVAILABLE
+ }
+ val oldPolicy = StrictMode.allowThreadDiskWrites()
+ try {
+ val route = ChimeraConfigManager.findComponentByComponentName(currentClassName)
+ if (route == null) {
+ Log.w(TAG, "Chimera component route not found for $currentClassName (prefix=${ChimeraConfigManager.getChimeraPrefix()})")
+ return DynamicComponentResolution.UNAVAILABLE
+ }
+
+ val module = ChimeraConfigManager.findModuleByComponent(currentClassName)
+ if (module == null) {
+ Log.w(TAG, "Chimera module not found for $currentClassName")
+ return DynamicComponentResolution.UNAVAILABLE
+ }
+ val routeModuleId = route.moduleId.orEmpty()
+ val verifiedRoute = routeModuleId.isNotEmpty() &&
+ ChimeraApkManifestReader.readVerifiedCapabilities(this, module).any { capability ->
+ capability.moduleId == routeModuleId &&
+ capability.activityBindings.any { binding ->
+ binding.containerName == route.containerName &&
+ binding.moduleChimeraName == route.moduleChimeraName
+ }
+ }
+ if (!verifiedRoute) {
+ Log.w(TAG, "Chimera activity route is not declared by the verified module APK: $currentClassName")
+ return DynamicComponentResolution.UNAVAILABLE
+ }
+
+ // Permission is part of the module's execution precondition. Check it before creating
+ // the module ClassLoader or invoking ModuleApi initialization so revoked permissions
+ // cannot run module code while the authorization page is being shown.
+ val requestedFeatureNames =
+ ModuleDownloadRegistry.requestedFeatureNamesForActivity(this, currentClassName)
+ if (ModuleDownloadRegistry.hasMissingPermissions(this, requestedFeatureNames)) {
+ Log.w(TAG, "Required permission missing for installed module: $currentClassName")
+ return DynamicComponentResolution.PERMISSION_REQUIRED
+ }
+
+ val moduleVersion = module.moduleVersion?.toIntOrNull() ?: 0
+ val moduleData = try {
+ ChimeraModuleLdr.loadModule(this, routeModuleId, module.moduleName, moduleVersion)
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to load module for $currentClassName, falling back", e)
+ null
+ }
+ if (moduleData == null) {
+ Log.w(TAG, "Module not available for $currentClassName, will use fallback")
+ return DynamicComponentResolution.UNAVAILABLE
+ }
+
+ var method: Method? = null
+ var constructor: Constructor<*>? = null
+ try {
+ val instanceProviderClass = moduleData.classLoader.loadClass("${ChimeraConfigManager.getChimeraPrefix()}${route.moduleChimeraName}").asSubclass(InstanceProvider::class.java)
+
+ try {
+ constructor = instanceProviderClass.getConstructor()
+ } catch (_: NoSuchMethodException) {
+ try {
+ method = instanceProviderClass.getDeclaredMethod("provideInstance")
+ } catch (_: NoSuchMethodException) {
+ }
+ }
+
+ val callback = createInstance(
+ Activity::class.java, constructor, arrayOf(), method
+ ) as? ChimeraProxyCallback ?: return DynamicComponentResolution.UNAVAILABLE
+
+ ChimeraComponentProxy.bindComponentProxy(this, this, callback, moduleData)
+ Log.d(TAG, "instanceProviderClass: $instanceProviderClass")
+ return DynamicComponentResolution.LOADED
+ } catch (e: ClassNotFoundException) {
+ Log.w(TAG, "instanceProviderClass error: $e", e)
+ } catch (e: NoClassDefFoundError) {
+ Log.w(TAG, "instanceProviderClass dependency missing: ${e.message}", e)
+ }
+
+ return DynamicComponentResolution.UNAVAILABLE
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy)
+ }
+ }
+
+ private fun createInstance(clazz: Class<*>, constructor: Constructor<*>?, arguments: Array, method: Method?): Any? {
+ val instanceProvider = runCatching {
+ constructor?.let {
+ constructor.newInstance(*arguments) as InstanceProvider
+ } ?: method?.let {
+ method.invoke(null, null) as? InstanceProvider
+ }
+ }.getOrElse {
+ Log.w(TAG, "Failed to instantiate: provideInstance() returned null")
+ return null
+ }
+
+ if (instanceProvider == null) {
+ Log.w(TAG, "Failed to instantiate: provideInstance() returned null")
+ return null
+ }
+
+ return try {
+ clazz.cast(instanceProvider.getChimeraImpl())
+ } catch (e: ClassCastException) {
+ Log.w(TAG, "Failed to cast to $clazz")
+ return null
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ hasFeatureRequest = savedInstanceState?.getBoolean(STATE_FEATURE_REQUEST) ?: true
+
+ if (!hasActivityImpl) {
+ Log.w(TAG, "onCreate: module not loaded, falling back to container lifecycle")
+ super.onCreate(savedInstanceState)
+ return
+ }
+
+ val moduleState = extractModuleState(savedInstanceState)
+ setBundleClassLoader(moduleState)
+ getChimeraActivity().public_onCreate(moduleState)
+ }
+
+ override fun onResume() {
+ val currentImpl = activityImpl
+ val isLoadedDynamicImpl = currentImpl != null &&
+ currentImpl !is ChimeraFallbackActImpl &&
+ currentImpl !is ChimeraPermissionActImpl
+ if (isLoadedDynamicImpl && !DynamicModuleSettings.isAvailable(this)) {
+ Log.w(TAG, "Dynamic modules were disabled while ${javaClass.name} was stopped")
+ // Complete the platform lifecycle without dispatching into disabled module code.
+ super.platform_onResume()
+ super.platform_finish()
+ return
+ }
+ if (!permissionRedirectLaunched && isLoadedDynamicImpl) {
+ val currentClassName = javaClass.name
+ val requestedFeatureNames =
+ ModuleDownloadRegistry.requestedFeatureNamesForActivity(this, currentClassName)
+ if (ModuleDownloadRegistry.hasMissingPermissions(this, requestedFeatureNames)) {
+ val permissionIntent = ModuleDownloadRegistry.createModulePermissionIntent(
+ this,
+ requestedFeatureNames
+ )
+ if (permissionIntent != null) {
+ permissionRedirectLaunched = true
+ // Satisfy the platform lifecycle without resuming module code after permission revocation.
+ super.platform_onResume()
+ super.platform_startActivityForResult(permissionIntent, REQUEST_MODULE_PERMISSION)
+ return
+ }
+ Log.w(TAG, "Unable to create permission gate for installed module: $currentClassName")
+ }
+ }
+ super.onResume()
+ }
+
+ override fun createContextWrapper(proxy: Any?, context: Context): Context {
+ return GmsContextWrapper(context)
+ }
+
+ override fun attachBaseContext(newBase: Context) {
+ try {
+ isAttachingBaseContext = true
+ super.attachBaseContext(newBase)
+ ChimeraModuleBootstrap.ensureInitialized(newBase)
+ runCatching { ChimeraConfigManager.reload() }
+ when (resolveDynamicComponent()) {
+ DynamicComponentResolution.LOADED -> Unit
+ DynamicComponentResolution.PERMISSION_REQUIRED ->
+ bindContainerActivityImpl(newBase, getChimeraPermissionActImpl())
+ DynamicComponentResolution.UNAVAILABLE ->
+ bindContainerActivityImpl(newBase, getChimeraFallbackActImpl())
+ }
+ } finally {
+ isAttachingBaseContext = false
+ }
+ }
+
+ private fun bindContainerActivityImpl(context: Context, callback: ChimeraProxyCallback) {
+ val applicationContext = context.applicationContext
+ val moduleContext = ModuleContext(
+ context,
+ ModuleContext.createApkApplicationContext(
+ applicationContext,
+ ContainerApk(context),
+ null,
+ applicationContext.classLoader,
+ emptyMap()
+ ),
+ "",
+ -1,
+ null,
+ null,
+ )
+ ChimeraComponentProxy.bindComponentProxy(this, this, callback, moduleContext)
+ }
+
+ open fun getChimeraFallbackActImpl(): ChimeraFallbackActImpl {
+ return ChimeraFallbackActImpl()
+ }
+
+ open fun getChimeraPermissionActImpl(): ChimeraPermissionActImpl {
+ return ChimeraPermissionActImpl()
+ }
+
+ override fun getChimeraActivity(): Activity {
+ return activityImpl ?: throw IllegalStateException("Activity impl has not been set!")
+ }
+
+ private val hasActivityImpl: Boolean get() = activityImpl != null
+
+ override fun clearFeatureRequest() {
+ hasFeatureRequest = false
+ }
+
+ override fun hasFeatureRequest(): Boolean {
+ return hasFeatureRequest
+ }
+
+ override fun getTheme(): Resources.Theme {
+ return if (isAttachingBaseContext || !hasActivityImpl) {
+ super.getTheme()
+ } else {
+ getChimeraActivity().theme
+ }
+ }
+
+ override fun setTheme(resid: Int) {
+ if (!hasActivityImpl) {
+ super.setTheme(resid)
+ return
+ }
+ val newResId = findModuleThemeResId(getChimeraActivity(), resid)
+ super.setTheme(newResId)
+ getChimeraActivity().setTheme(newResId)
+ }
+
+ private fun findModuleThemeResId(activity: Activity, resId: Int): Int {
+ if (resId == 0) return 0
+
+ var currentResId = resId
+ val visited = mutableSetOf()
+
+ while (!visited.contains(currentResId)) {
+ try {
+ val moduleResId = ChimeraResource.getResourceId(
+ moduleClassLoader, activity.resources, super.getResources(), currentResId
+ )
+ if (moduleResId != 0) return moduleResId
+ } catch (_: Resources.NotFoundException) {
+ }
+
+ visited.add(currentResId)
+ currentResId = getThemeFallback(currentResId)
+ }
+
+ Log.w(TAG, "Failed to find module theme for container theme: $resId (tried: $visited)")
+ return 0
+ }
+
+ protected open fun getThemeFallback(themeResId: Int): Int = themeResId
+
+ override fun platform_getReferrer(): Uri? {
+ val referrer = super.platform_getReferrer()
+ Log.d(TAG, "platform_getReferrer: $referrer, callingPackage=$callingPackage, callingActivity=$callingActivity")
+
+ if (referrer != null) return referrer
+
+ val intentReferrer = intent?.getParcelableExtra("android.intent.extra.REFERRER")
+ if (intentReferrer != null) {
+ Log.d(TAG, "platform_getReferrer: using intent extra referrer: $intentReferrer")
+ return intentReferrer
+ }
+
+ val referrerName = intent?.getStringExtra("android.intent.extra.REFERRER_NAME")
+ if (referrerName != null) {
+ Log.d(TAG, "platform_getReferrer: using intent extra referrer name: $referrerName")
+ return Uri.parse(referrerName)
+ }
+
+ val pkg = callingPackage
+ if (pkg != null) {
+ val uri = Uri.parse("android-app://$pkg")
+ Log.d(TAG, "platform_getReferrer: constructed from callingPackage: $uri")
+ return uri
+ }
+
+ Log.w(TAG, "platform_getReferrer: no referrer available")
+ return null
+ }
+
+ override fun getAssets(): AssetManager {
+ return this.resources.assets
+ }
+
+ override fun getClassLoader(): ClassLoader {
+ return if (this.isAttachingBaseContext || !this::currentClassLoader.isInitialized || !hasActivityImpl) {
+ super.getClassLoader()
+ } else {
+ this.currentClassLoader
+ }
+ }
+
+ override fun getResources(): Resources {
+ return if (this.isAttachingBaseContext || !hasActivityImpl) super.getResources() else this.getChimeraActivity().resources
+ }
+
+ override fun getSystemService(serviceName: String): Any? {
+ if (!isAttachingBaseContext && hasActivityImpl && "layout_inflater".equals(serviceName)) {
+ if (layoutInflater == null) {
+ layoutInflater = (super.getSystemService(serviceName) as LayoutInflater).cloneInContext(getChimeraActivity())
+ }
+ return layoutInflater
+ }
+ return super.getSystemService(serviceName)
+ }
+
+ override fun platform_onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
+ if ("fragment" == name) {
+ Log.w(TAG, "Chimera does not support inflating fragments via XML at this time.")
+ return null
+ }
+ if (this::moduleClassLoader.isInitialized) {
+ val view = ChimeraViewCreator.createView(moduleClassLoader, context, name, attrs)
+ if (view != null) return view
+ }
+ return super.platform_onCreateView(parent, name, context, attrs)
+ }
+
+ override fun platform_onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ if ("fragment" == name) {
+ Log.w(TAG, "Chimera does not support inflating fragments via XML at this time.")
+ return null
+ }
+ if (this::moduleClassLoader.isInitialized) {
+ val view = ChimeraViewCreator.createView(moduleClassLoader, context, name, attrs)
+ if (view != null) return view
+ }
+ return super.platform_onCreateView(name, context, attrs)
+ }
+
+ @SuppressLint("MissingSuperCall")
+ override fun onSaveInstanceState(outState: Bundle) {
+ if (!hasActivityImpl) return
+ val moduleState = Bundle()
+ getChimeraActivity().public_onSaveInstanceState(moduleState)
+ outState.putBundle(STATE_MODULE, moduleState)
+ outState.putBoolean(STATE_FEATURE_REQUEST, hasFeatureRequest)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ if (!hasActivityImpl) {
+ super.platform_onRestoreInstanceState(Bundle()); return
+ }
+ val moduleState = extractModuleState(savedInstanceState)
+ if (moduleState == null) {
+ super.platform_onRestoreInstanceState(Bundle())
+ return
+ }
+ getChimeraActivity().public_onRestoreInstanceState(moduleState)
+ }
+
+ @SuppressLint("MissingSuperCall")
+ override fun onPostCreate(savedInstanceState: Bundle?) {
+ if (!hasActivityImpl) {
+ super.onPostCreate(savedInstanceState)
+ return
+ }
+ // The module implementation calls platform_onPostCreate() through its default superclass.
+ // Calling BaseActivityProxy.onPostCreate() here would dispatch to the module once already,
+ // then the explicit call below would dispatch a second time.
+ val moduleState = if (savedInstanceState != null) extractModuleState(savedInstanceState) else null
+ getChimeraActivity().public_onPostCreate(moduleState)
+ }
+
+ private fun extractModuleState(savedInstanceState: Bundle?): Bundle? {
+ if (savedInstanceState == null) return null
+ return savedInstanceState.getBundle(STATE_MODULE)
+ }
+
+ private fun setBundleClassLoader(bundle: Bundle?) {
+ if (bundle != null && this::moduleClassLoader.isInitialized) {
+ bundle.classLoader = moduleClassLoader
+ }
+ }
+
+ private fun setIntentClassLoader(intent: Intent?) {
+ if (intent != null && this::moduleClassLoader.isInitialized) {
+ intent.setExtrasClassLoader(moduleClassLoader)
+ }
+ }
+
+ override fun platform_getIntent(): Intent? {
+ val intent = super.platform_getIntent()
+ setIntentClassLoader(intent)
+ return intent
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == REQUEST_MODULE_PERMISSION) {
+ permissionRedirectLaunched = false
+ if (resultCode == android.app.Activity.RESULT_OK) {
+ // Keep this Activity record so callingPackage and the caller's result target are preserved.
+ super.platform_recreate()
+ } else {
+ super.platform_finish()
+ }
+ return
+ }
+ if (!hasActivityImpl) return
+ val activity = getChimeraActivity()
+ if (data != null) {
+ setIntentClassLoader(data)
+ if (data.hasExtra("_chimera_fallback_only") && activity !is ChimeraFallbackActImpl) {
+ return
+ }
+ }
+ activity.public_onActivityResult(requestCode, resultCode, data)
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ if (!hasActivityImpl) return
+ setIntentClassLoader(intent)
+ getChimeraActivity().public_onNewIntent(intent)
+ }
+
+ @Suppress("DEPRECATION")
+ override fun platform_overridePendingTransition(enterAnim: Int, exitAnim: Int) {
+ if (this::containerClassLoader.isInitialized && hasActivityImpl) {
+ val containerRes = super.getResources()
+ val moduleRes = getChimeraActivity().resources
+ super.platform_overridePendingTransition(
+ ChimeraResource.getResourceId(containerClassLoader, containerRes, moduleRes, enterAnim), ChimeraResource.getResourceId(containerClassLoader, containerRes, moduleRes, exitAnim)
+ )
+ } else {
+ super.platform_overridePendingTransition(enterAnim, exitAnim)
+ }
+ }
+
+ override fun createModuleContext(module: Any?, moduleClass: Class<*>?, context: Context): Context {
+ return createContextWrapper(module, context)
+ }
+
+ override fun setProxyWrapper(proxy: Any?, context: Context) {
+ check(activityImpl == null) { "Activity impl has been set!" }
+ activityImpl = proxy as Activity
+ activityProxyWrapper = proxy
+ moduleClassLoader = context.classLoader
+ checkNotNull(javaClass.classLoader) { "Container ClassLoader not been set!" }
+ containerClassLoader = javaClass.classLoader
+ currentClassLoader = moduleClassLoader
+ }
+
+ override fun setProxyWrapper(moduleName: String?, proxy: Any?, context: Context) {
+ this.setProxyWrapper(proxy, context)
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/android/IActivityProxy.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/IActivityProxy.kt
new file mode 100644
index 0000000000..80676937a5
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/IActivityProxy.kt
@@ -0,0 +1,663 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.android
+
+import android.app.ActionBar
+import android.app.Activity
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.Application
+import android.app.ComponentCaller
+import android.app.Fragment
+import android.app.FragmentManager
+import android.app.LoaderManager
+import android.app.PendingIntent
+import android.app.PictureInPictureParams
+import android.app.SharedElementCallback
+import android.app.TaskStackBuilder
+import android.app.VoiceInteractor
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.content.LocusId
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.database.Cursor
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import android.media.session.MediaController
+import android.net.Uri
+import android.os.Bundle
+import android.os.OutcomeReceiver
+import android.os.PersistableBundle
+import android.os.UserHandle
+import android.transition.Scene
+import android.transition.TransitionManager
+import android.util.AttributeSet
+import android.view.ActionMode
+import android.view.ContextMenu
+import android.view.DragAndDropPermissions
+import android.view.DragEvent
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.SearchEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
+import android.widget.Toolbar
+import android.window.OnBackInvokedDispatcher
+import android.window.SplashScreen
+
+interface IActivityProxy {
+ fun platform_addContentView(view: View?, params: ViewGroup.LayoutParams?)
+
+ fun platform_clearOverrideActivityTransition(transitionType: Int)
+
+ fun platform_closeContextMenu()
+
+ fun platform_closeOptionsMenu()
+
+ fun platform_convertFromTranslucent()
+
+ fun platform_convertToTranslucent(
+ listener: Any?,
+ options: ActivityOptions?
+ ): Boolean
+
+ fun platform_createPendingResult(requestCode: Int, data: Intent, flags: Int): PendingIntent
+
+ fun platform_dispatchGenericMotionEvent(event: MotionEvent?): Boolean
+
+ fun platform_dispatchKeyEvent(event: KeyEvent?): Boolean
+
+ fun platform_dispatchKeyShortcutEvent(event: KeyEvent?): Boolean
+
+ fun platform_dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean
+
+ fun platform_dispatchTouchEvent(event: MotionEvent?): Boolean
+
+ fun platform_dispatchTrackballEvent(event: MotionEvent?): Boolean
+
+ fun platform_findViewById(id: Int): T?
+
+ fun platform_finish()
+
+ fun platform_finishActivity(requestCode: Int)
+
+ @Deprecated("")
+ fun platform_finishActivityFromChild(child: Activity, requestCode: Int)
+
+ fun platform_finishAffinity()
+
+ fun platform_finishAfterTransition()
+
+ fun platform_finishAndRemoveTask()
+
+ @Deprecated("")
+ fun platform_finishFromChild(child: Activity?)
+
+ fun platform_getActionBar(): ActionBar?
+
+ fun platform_getApplication(): Application?
+
+ fun platform_getCaller(): ComponentCaller?
+
+ fun platform_getCallingActivity(): ComponentName?
+
+ fun platform_getCallingPackage(): String?
+
+ fun platform_getChangingConfigurations(): Int
+
+ fun platform_getComponentName(): ComponentName?
+
+ fun platform_getContentScene(): Scene?
+
+ fun platform_getContentTransitionManager(): TransitionManager?
+
+ fun platform_getCurrentCaller(): ComponentCaller
+
+ fun platform_getCurrentFocus(): View?
+
+ @Deprecated("")
+ fun platform_getFragmentManager(): FragmentManager?
+
+ fun platform_getInitialCaller(): ComponentCaller
+
+ fun platform_getIntent(): Intent?
+
+ fun platform_getLastNonConfigurationInstance(): Any?
+
+ fun platform_getLaunchedFromPackage(): String?
+
+ fun platform_getLaunchedFromUid(): Int
+
+ fun platform_getLayoutInflater(): LayoutInflater
+
+ @Deprecated("")
+ fun platform_getLoaderManager(): LoaderManager?
+
+ fun platform_getLocalClassName(): String
+
+ fun platform_getMaxNumPictureInPictureActions(): Int
+
+ fun platform_getMediaController(): MediaController
+
+ fun platform_getMenuInflater(): MenuInflater
+
+ fun platform_getOnBackInvokedDispatcher(): OnBackInvokedDispatcher
+
+ @Deprecated("")
+ fun platform_getParent(): Activity
+
+ fun platform_getParentActivityIntent(): Intent?
+
+ fun platform_getPreferences(mode: Int): SharedPreferences?
+
+ fun platform_getReferrer(): Uri?
+
+ fun platform_getRequestedOrientation(): Int
+
+ fun platform_getSearchEvent(): SearchEvent?
+
+ fun platform_getSplashScreen(): SplashScreen
+
+ fun platform_getTaskId(): Int
+
+ fun platform_getTitle(): CharSequence?
+
+ fun platform_getTitleColor(): Int
+
+ fun platform_getVoiceInteractor(): VoiceInteractor?
+
+ fun platform_getVolumeControlStream(): Int
+
+ fun platform_getWindow(): Window?
+
+ fun platform_getWindowManager(): WindowManager?
+
+ fun platform_hasWindowFocus(): Boolean
+
+ fun platform_invalidateOptionsMenu()
+
+ fun platform_isActivityTransitionRunning(): Boolean
+
+ @Deprecated("")
+ fun platform_isBackgroundVisibleBehind(): Boolean
+
+ fun platform_isChangingConfigurations(): Boolean
+
+ @Deprecated("")
+ fun platform_isChild(): Boolean
+
+ fun platform_isDestroyed(): Boolean
+
+ fun platform_isFinishing(): Boolean
+
+ fun platform_isImmersive(): Boolean
+
+ fun platform_isInMultiWindowMode(): Boolean
+
+ fun platform_isInPictureInPictureMode(): Boolean
+
+ fun platform_isLaunchedFromBubble(): Boolean
+
+ fun platform_isLocalVoiceInteractionSupported(): Boolean
+
+ fun platform_isTaskRoot(): Boolean
+
+ fun platform_isVoiceInteraction(): Boolean
+
+ fun platform_isVoiceInteractionRoot(): Boolean
+
+ @Deprecated("")
+ fun platform_managedQuery(
+ uri: Uri?,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?
+ ): Cursor?
+
+ fun platform_moveTaskToBack(nonRoot: Boolean): Boolean
+
+ fun platform_navigateUpTo(intent: Intent?): Boolean
+
+ @Deprecated("")
+ fun platform_navigateUpToFromChild(child: Activity?, intent: Intent?): Boolean
+
+ fun platform_onActionModeFinished(mode: ActionMode?)
+
+ fun platform_onActionModeStarted(mode: ActionMode?)
+
+ fun platform_onActivityReenter(resultCode: Int, data: Intent?)
+
+ fun platform_onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
+
+ fun platform_onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, caller: ComponentCaller)
+
+ @Deprecated("")
+ fun platform_onAttachFragment(fragment: Fragment?)
+
+ fun platform_onAttachedToWindow()
+
+ @Deprecated("")
+ fun platform_onBackPressed()
+
+ @Deprecated("Deprecated in Java")
+ fun platform_onBackgroundVisibleBehindChanged(visible: Boolean)
+
+ fun platform_onChildTitleChanged(child: Activity?, title: CharSequence?)
+
+ fun platform_onConfigurationChanged(newConfig: Configuration)
+
+ fun platform_onContentChanged()
+
+ fun platform_onContextItemSelected(item: MenuItem): Boolean
+
+ fun platform_onContextMenuClosed(menu: Menu)
+
+ fun platform_onCreate(savedInstanceState: Bundle?)
+
+ fun platform_onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?)
+
+ fun platform_onCreateContextMenu(
+ menu: ContextMenu?,
+ view: View?,
+ menuInfo: ContextMenu.ContextMenuInfo?
+ )
+
+ fun platform_onCreateDescription(): CharSequence?
+
+ fun platform_onCreateNavigateUpTaskStack(builder: TaskStackBuilder?)
+
+ fun platform_onCreateOptionsMenu(menu: Menu): Boolean
+
+ fun platform_onCreatePanelMenu(featureId: Int, menu: Menu): Boolean
+
+ fun platform_onCreatePanelView(featureId: Int): View?
+
+ @Deprecated("")
+ fun platform_onCreateThumbnail(outBitmap: Bitmap?, canvas: Canvas?): Boolean
+
+ fun platform_onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View?
+
+ fun platform_onCreateView(name: String, context: Context, attrs: AttributeSet): View?
+
+ fun platform_onDestroy()
+
+ fun platform_onDetachedFromWindow()
+
+ fun platform_onGenericMotionEvent(event: MotionEvent?): Boolean
+
+ fun platform_onKeyDown(keyCode: Int, event: KeyEvent?): Boolean
+
+ fun platform_onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean
+
+ fun platform_onKeyMultiple(keyCode: Int, repeatCount: Int, event: KeyEvent?): Boolean
+
+ fun platform_onKeyShortcut(keyCode: Int, event: KeyEvent?): Boolean
+
+ fun platform_onKeyUp(keyCode: Int, event: KeyEvent?): Boolean
+
+ fun platform_onLocalVoiceInteractionStarted()
+
+ fun platform_onLocalVoiceInteractionStopped()
+
+ fun platform_onMenuItemSelected(featureId: Int, item: MenuItem): Boolean
+
+ fun platform_onMenuOpened(featureId: Int, menu: Menu): Boolean
+
+ fun platform_onNavigateUp(): Boolean
+
+ @Deprecated("")
+ fun platform_onNavigateUpFromChild(child: Activity?): Boolean
+
+ fun platform_onNewIntent(intent: Intent?)
+
+ fun platform_onNewIntent(intent: Intent, caller: ComponentCaller)
+
+ fun platform_onOptionsItemSelected(item: MenuItem): Boolean
+
+ fun platform_onOptionsMenuClosed(menu: Menu)
+
+ fun platform_onPanelClosed(featureId: Int, menu: Menu)
+
+ fun platform_onPause()
+
+ fun platform_onPointerCaptureChanged(hasCapture: Boolean)
+
+ fun platform_onPostCreate(savedInstanceState: Bundle?)
+
+ fun platform_onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?)
+
+ fun platform_onPostResume()
+
+ fun platform_onPrepareNavigateUpTaskStack(builder: TaskStackBuilder?)
+
+ fun platform_onPrepareOptionsMenu(menu: Menu): Boolean
+
+ fun platform_onPreparePanel(featureId: Int, view: View?, menu: Menu): Boolean
+
+ fun platform_onProvideReferrer(): Uri?
+
+ fun platform_onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray)
+
+ fun platform_onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray, userId: Int)
+
+ fun platform_onRestart()
+
+ fun platform_onRestoreInstanceState(savedInstanceState: Bundle)
+
+ fun platform_onRestoreInstanceState(savedInstanceState: Bundle?, persistentState: PersistableBundle?)
+
+ fun platform_onResume()
+
+ fun platform_onSaveInstanceState(outState: Bundle)
+
+ fun platform_onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle)
+
+ fun platform_onSearchRequested(): Boolean
+
+ fun platform_onSearchRequested(event: SearchEvent?): Boolean
+
+ fun platform_onStart()
+
+ @Deprecated("")
+ fun platform_onStateNotSaved()
+
+ fun platform_onStop()
+
+ fun platform_onTitleChanged(title: CharSequence?, color: Int)
+
+ fun platform_onTouchEvent(event: MotionEvent?): Boolean
+
+ fun platform_onTrackballEvent(event: MotionEvent?): Boolean
+
+ fun platform_onUserInteraction()
+
+ fun platform_onUserLeaveHint()
+
+ @Deprecated("")
+ fun platform_onVisibleBehindCanceled()
+
+ fun platform_onWindowAttributesChanged(params: WindowManager.LayoutParams?)
+
+ fun platform_onWindowFocusChanged(hasFocus: Boolean)
+
+ fun platform_onWindowStartingActionMode(callback: ActionMode.Callback?): ActionMode?
+
+ fun platform_onWindowStartingActionMode(callback: ActionMode.Callback?, type: Int): ActionMode?
+
+ fun platform_openContextMenu(view: View?)
+
+ fun platform_openOptionsMenu()
+
+ fun platform_overrideActivityTransition(transitionType: Int, enterAnim: Int, exitAnim: Int)
+
+ fun platform_overrideActivityTransition(
+ transitionType: Int,
+ enterAnim: Int,
+ exitAnim: Int,
+ backgroundColor: Int
+ )
+
+ @Deprecated("")
+ fun platform_overridePendingTransition(enterAnim: Int, exitAnim: Int)
+
+ @Deprecated("")
+ fun platform_overridePendingTransition(enterAnim: Int, exitAnim: Int, backgroundColor: Int)
+
+ fun platform_postponeEnterTransition()
+
+ fun platform_recreate()
+
+ fun platform_registerActivityLifecycleCallbacks(callback: Application.ActivityLifecycleCallbacks)
+
+ fun platform_registerForContextMenu(view: View?)
+
+ fun platform_releaseInstance(): Boolean
+
+ fun platform_reportFullyDrawn()
+
+ fun platform_requestDragAndDropPermissions(event: DragEvent?): DragAndDropPermissions?
+
+ fun platform_requestFullscreenMode(request: Int, approvalCallback: OutcomeReceiver?)
+
+ fun platform_requestPermissions(permissions: Array, requestCode: Int)
+
+ fun platform_requestPermissions(permissions: Array, requestCode: Int, userId: Int)
+
+ @Deprecated("")
+ fun platform_requestVisibleBehind(visible: Boolean): Boolean
+
+ fun platform_requestWindowFeature(featureId: Int): Boolean
+
+ fun platform_requireViewById(id: Int): View?
+
+ fun platform_runOnUiThread(action: Runnable?)
+
+ fun platform_setActionBar(toolbar: Toolbar?)
+
+ fun platform_setAllowCrossUidActivitySwitchFromBelow(allow: Boolean)
+
+ fun platform_setContentTransitionManager(transitionManager: TransitionManager?)
+
+ fun platform_setContentView(layoutResID: Int)
+
+ fun platform_setContentView(view: View?)
+
+ fun platform_setContentView(view: View?, params: ViewGroup.LayoutParams?)
+
+ fun platform_setDefaultKeyMode(mode: Int)
+
+ fun platform_setEnterSharedElementCallback(callback: SharedElementCallback?)
+
+ fun platform_setExitSharedElementCallback(callback: SharedElementCallback?)
+
+ fun platform_setFeatureDrawable(featureId: Int, drawable: Drawable?)
+
+ fun platform_setFeatureDrawableAlpha(featureId: Int, alpha: Int)
+
+ fun platform_setFeatureDrawableResource(featureId: Int, resId: Int)
+
+ fun platform_setFeatureDrawableUri(featureId: Int, uri: Uri?)
+
+ fun platform_setFinishOnTouchOutside(finish: Boolean)
+
+ fun platform_setImmersive(immersive: Boolean)
+
+ fun platform_setInheritShowWhenLocked(showWhenLocked: Boolean)
+
+ fun platform_setIntent(intent: Intent?)
+
+ fun platform_setIntent(intent: Intent?, caller: ComponentCaller?)
+
+ fun platform_setLocusContext(locusId: LocusId?, bundle: Bundle?)
+
+ fun platform_setMediaController(controller: MediaController?)
+
+ fun platform_setPictureInPictureParams(params: PictureInPictureParams)
+
+ @Deprecated("")
+ fun platform_setProgress(progress: Int)
+
+ @Deprecated("")
+ fun platform_setProgressBarIndeterminate(indeterminate: Boolean)
+
+ @Deprecated("")
+ fun platform_setProgressBarIndeterminateVisibility(visible: Boolean)
+
+ @Deprecated("")
+ fun platform_setProgressBarVisibility(visible: Boolean)
+
+ fun platform_setRecentsScreenshotEnabled(enabled: Boolean)
+
+ fun platform_setRequestedOrientation(orientation: Int)
+
+ fun platform_setResult(resultCode: Int)
+
+ fun platform_setResult(resultCode: Int, data: Intent?)
+
+ @Deprecated("")
+ fun platform_setSecondaryProgress(secondaryProgress: Int)
+
+ fun platform_setShouldDockBigOverlays(shouldDock: Boolean)
+
+ fun platform_setShowWhenLocked(showWhenLocked: Boolean)
+
+ fun platform_setTaskDescription(description: ActivityManager.TaskDescription?)
+
+ fun platform_setTitle(titleId: Int)
+
+ fun platform_setTitle(title: CharSequence?)
+
+ @Deprecated("")
+ fun platform_setTitleColor(textColor: Int)
+
+ fun platform_setTranslucent(translucent: Boolean): Boolean
+
+ fun platform_setTurnScreenOn(turnScreenOn: Boolean)
+
+ fun platform_setVisible(visible: Boolean)
+
+ fun platform_setVolumeControlStream(streamType: Int)
+
+ fun platform_setVrModeEnabled(enabled: Boolean, requestedComponent: ComponentName)
+
+ fun platform_shouldDockBigOverlays(): Boolean
+
+ fun platform_shouldShowRequestPermissionRationale(permission: String): Boolean
+
+ fun platform_shouldShowRequestPermissionRationale(permission: String, userId: Int): Boolean
+
+ fun platform_shouldUpRecreateTask(targetIntent: Intent?): Boolean
+
+ fun platform_showAssist(args: Bundle?): Boolean
+
+ fun platform_showLockTaskEscapeMessage()
+
+ fun platform_startActionMode(callback: ActionMode.Callback?): ActionMode?
+
+ fun platform_startActionMode(callback: ActionMode.Callback?, type: Int): ActionMode?
+
+ fun platform_startActivities(intents: Array)
+
+ fun platform_startActivities(intents: Array, options: Bundle?)
+
+ fun platform_startActivity(intent: Intent?)
+
+ fun platform_startActivity(intent: Intent?, options: Bundle?)
+
+ fun platform_startActivityForResult(intent: Intent?, requestCode: Int)
+
+ fun platform_startActivityForResult(intent: Intent?, requestCode: Int, options: Bundle?)
+
+ fun platform_startActivityForResultAsUser(
+ intent: Intent?,
+ requestCode: Int,
+ options: Bundle?,
+ user: UserHandle?
+ )
+
+ fun platform_startActivityForResultAsUser(intent: Intent?, requestCode: Int, user: UserHandle?)
+
+ fun platform_startActivityForResultAsUser(
+ intent: Intent?,
+ permission: String?,
+ requestCode: Int,
+ options: Bundle?,
+ user: UserHandle?
+ )
+
+ @Deprecated("")
+ fun platform_startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int)
+
+ @Deprecated("")
+ fun platform_startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int, options: Bundle?)
+
+ @Deprecated("")
+ fun platform_startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int)
+
+ @Deprecated("")
+ fun platform_startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int, options: Bundle?)
+
+ fun platform_startActivityIfNeeded(intent: Intent, requestCode: Int): Boolean
+
+ fun platform_startActivityIfNeeded(intent: Intent, requestCode: Int, options: Bundle?): Boolean
+
+ fun platform_startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int)
+
+ fun platform_startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?)
+
+ fun platform_startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int)
+
+ fun platform_startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?)
+
+ @Deprecated("")
+ fun platform_startIntentSenderFromChild(
+ child: Activity?,
+ intentSender: IntentSender?,
+ requestCode: Int,
+ fillInIntent: Intent?,
+ flagsMask: Int,
+ flagsValues: Int,
+ extraFlags: Int
+ )
+
+ @Deprecated("")
+ fun platform_startIntentSenderFromChild(
+ child: Activity?,
+ intentSender: IntentSender?,
+ requestCode: Int,
+ fillInIntent: Intent?,
+ flagsMask: Int,
+ flagsValues: Int,
+ extraFlags: Int,
+ options: Bundle?
+ )
+
+ fun platform_startLocalVoiceInteraction(privateOptions: Bundle?)
+
+ fun platform_startLockTask()
+
+ @Deprecated("")
+ fun platform_startManagingCursor(cursor: Cursor?)
+
+ fun platform_startNextMatchingActivity(intent: Intent): Boolean
+
+ fun platform_startNextMatchingActivity(intent: Intent, options: Bundle?): Boolean
+
+ fun platform_startPostponedEnterTransition()
+
+ fun platform_startSearch(
+ initialQuery: String?,
+ selectInitialQuery: Boolean,
+ appSearchData: Bundle?,
+ globalSearch: Boolean
+ )
+
+ fun platform_stopLocalVoiceInteraction()
+
+ fun platform_stopLockTask()
+
+ @Deprecated("")
+ fun platform_stopManagingCursor(cursor: Cursor?)
+
+ fun platform_takeKeyEvents(get: Boolean)
+
+ fun platform_triggerSearch(query: String?, appSearchData: Bundle?)
+
+ fun platform_unregisterActivityLifecycleCallbacks(callback: Application.ActivityLifecycleCallbacks)
+
+ fun platform_unregisterForContextMenu(view: View?)
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/android/IChimeraActivityProxy.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/IChimeraActivityProxy.kt
new file mode 100644
index 0000000000..106fcddbc9
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/android/IChimeraActivityProxy.kt
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.android
+
+interface IChimeraActivityProxy : IActivityProxy {
+ fun getChimeraActivity(): Activity
+ fun clearFeatureRequest()
+ fun hasFeatureRequest(): Boolean
+ fun getSystemService(serviceName: String): Any?
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/annotation/ChimeraApiVersion.java b/play-services-chimera-core/src/main/java/com/google/android/chimera/annotation/ChimeraApiVersion.java
new file mode 100644
index 0000000000..f986c4ae45
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/annotation/ChimeraApiVersion.java
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@ChimeraApiVersion(added = 101)
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ChimeraApiVersion {
+ long added();
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/BaseActivityProxy.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/BaseActivityProxy.kt
new file mode 100644
index 0000000000..c0aefdef9f
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/BaseActivityProxy.kt
@@ -0,0 +1,2193 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.app.ActionBar
+import android.app.Activity
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.Application
+import android.app.ComponentCaller
+import android.app.Fragment
+import android.app.FragmentManager
+import android.app.LoaderManager
+import android.app.PendingIntent
+import android.app.PictureInPictureParams
+import android.app.SharedElementCallback
+import android.app.TaskStackBuilder
+import android.app.VoiceInteractor
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.content.LocusId
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.database.Cursor
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import android.media.session.MediaController
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.OutcomeReceiver
+import android.os.PersistableBundle
+import android.os.UserHandle
+import android.transition.Scene
+import android.transition.TransitionManager
+import android.util.AttributeSet
+import android.view.ActionMode
+import android.view.ContextMenu
+import android.view.DragAndDropPermissions
+import android.view.DragEvent
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.SearchEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
+import android.widget.Toolbar
+import android.window.OnBackInvokedDispatcher
+import android.window.SplashScreen
+import androidx.annotation.RequiresApi
+import com.google.android.chimera.android.ActivityProxyWrapper
+import com.google.android.chimera.android.IActivityProxy
+import java.lang.Deprecated
+
+open class BaseActivityProxy: Activity(), IActivityProxy {
+ open var activityProxyWrapper: ActivityProxyWrapper? = null
+
+ override fun addContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ activityProxyWrapper?.public_addContentView(view, params) ?: super.addContentView(view, params)
+ }
+
+ override fun platform_addContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ super.addContentView(view, params)
+ }
+
+ override fun clearOverrideActivityTransition(transitionType: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ activityProxyWrapper?.public_clearOverrideActivityTransition(transitionType) ?: super.clearOverrideActivityTransition(transitionType)
+ }
+ }
+
+ override fun platform_clearOverrideActivityTransition(transitionType: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ super.clearOverrideActivityTransition(transitionType)
+ }
+ }
+
+ override fun closeContextMenu() {
+ activityProxyWrapper?.public_closeContextMenu() ?: super.closeContextMenu()
+ }
+
+ override fun platform_closeContextMenu() {
+ super.closeContextMenu()
+ }
+
+ override fun closeOptionsMenu() {
+ activityProxyWrapper?.public_closeOptionsMenu() ?: super.closeOptionsMenu()
+ }
+
+ override fun platform_closeOptionsMenu() {
+ super.closeOptionsMenu()
+ }
+
+ fun convertFromTranslucent() {
+ activityProxyWrapper?.public_convertFromTranslucent() ?: throw UnsupportedOperationException("convertFromTranslucent is SystemApi")
+ }
+
+ override fun platform_convertFromTranslucent() {
+ throw UnsupportedOperationException("convertFromTranslucent is SystemApi")
+ }
+
+ fun convertToTranslucent(listener: Any?, options: ActivityOptions?): Boolean {
+ return activityProxyWrapper?.public_convertToTranslucent(listener, options) ?: throw UnsupportedOperationException("convertFromTranslucent is SystemApi")
+ }
+
+ override fun platform_convertToTranslucent(listener: Any?, options: ActivityOptions?): Boolean {
+ throw UnsupportedOperationException("convertFromTranslucent is SystemApi")
+ }
+
+ override fun createPendingResult(requestCode: Int, data: Intent, flags: Int): PendingIntent {
+ return activityProxyWrapper?.public_createPendingResult(requestCode, data, flags) ?: super.createPendingResult(requestCode, data, flags)
+ }
+
+ override fun platform_createPendingResult(requestCode: Int, data: Intent, flags: Int): PendingIntent {
+ return super.createPendingResult(requestCode, data, flags)
+ }
+
+ override fun dispatchGenericMotionEvent(event: MotionEvent?): Boolean {
+ return activityProxyWrapper?.public_dispatchGenericMotionEvent(event) ?: super.dispatchGenericMotionEvent(event)
+ }
+
+ override fun platform_dispatchGenericMotionEvent(event: MotionEvent?): Boolean {
+ return super.dispatchGenericMotionEvent(event)
+ }
+
+ override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
+ return activityProxyWrapper?.public_dispatchKeyEvent(event) ?: super.dispatchKeyEvent(event)
+ }
+
+ override fun platform_dispatchKeyEvent(event: KeyEvent?): Boolean {
+ return super.dispatchKeyEvent(event)
+ }
+
+ override fun dispatchKeyShortcutEvent(event: KeyEvent?): Boolean {
+ return activityProxyWrapper?.public_dispatchKeyShortcutEvent(event) ?: super.dispatchKeyShortcutEvent(event)
+ }
+
+ override fun platform_dispatchKeyShortcutEvent(event: KeyEvent?): Boolean {
+ return super.dispatchKeyShortcutEvent(event)
+ }
+
+ override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean {
+ return activityProxyWrapper?.public_dispatchPopulateAccessibilityEvent(event) ?: super.dispatchPopulateAccessibilityEvent(event)
+ }
+
+ override fun platform_dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean {
+ return super.dispatchPopulateAccessibilityEvent(event)
+ }
+
+ override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
+ return activityProxyWrapper?.public_dispatchTouchEvent(event) ?: super.dispatchTouchEvent(event)
+ }
+
+ override fun platform_dispatchTouchEvent(event: MotionEvent?): Boolean {
+ return super.dispatchTouchEvent(event)
+ }
+
+ override fun dispatchTrackballEvent(event: MotionEvent?): Boolean {
+ return activityProxyWrapper?.public_dispatchTrackballEvent(event) ?: super.dispatchTrackballEvent(event)
+ }
+ override fun platform_dispatchTrackballEvent(event: MotionEvent?): Boolean {
+ return super.dispatchTrackballEvent(event)
+ }
+
+ override fun findViewById(id: Int): T? {
+ return activityProxyWrapper?.public_findViewById(id) ?: super.findViewById(id)
+ }
+
+ override fun platform_findViewById(id: Int): T? {
+ return super.findViewById(id)
+ }
+
+ override fun finish() {
+ activityProxyWrapper?.public_finish() ?: super.finish()
+ }
+
+ override fun platform_finish() {
+ super.finish()
+ }
+
+ override fun finishActivity(requestCode: Int) {
+ activityProxyWrapper?.public_finishActivity(requestCode) ?: super.finishActivity(requestCode)
+ }
+
+ override fun platform_finishActivity(requestCode: Int) {
+ super.finishActivity(requestCode)
+ }
+
+ override fun finishActivityFromChild(child: Activity, requestCode: Int) {
+ activityProxyWrapper?.public_finishActivityFromChild(child, requestCode) ?: super.finishActivityFromChild(child, requestCode)
+ }
+
+ override fun platform_finishActivityFromChild(child: Activity, requestCode: Int) {
+ super.finishActivityFromChild(child, requestCode)
+ }
+
+ override fun finishAffinity() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ activityProxyWrapper?.public_finishAffinity() ?: super.finishAffinity()
+ }
+ }
+
+ override fun platform_finishAffinity() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ super.finishAffinity()
+ }
+ }
+
+ override fun finishAfterTransition() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activityProxyWrapper?.public_finishAfterTransition() ?: super.finishAfterTransition()
+ }
+ }
+
+ override fun platform_finishAfterTransition() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ super.finishAfterTransition()
+ }
+ }
+
+ override fun finishAndRemoveTask() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activityProxyWrapper?.public_finishAndRemoveTask() ?: super.finishAndRemoveTask()
+ }
+ }
+
+ override fun platform_finishAndRemoveTask() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ super.finishAndRemoveTask()
+ }
+ }
+
+ override fun finishFromChild(child: Activity?) {
+ activityProxyWrapper?.public_finishFromChild(child) ?: super.finishFromChild(child)
+ }
+
+ @Deprecated
+ override fun platform_finishFromChild(child: Activity?) {
+ super.finishFromChild(child)
+ }
+
+ override fun getActionBar(): ActionBar? {
+ return activityProxyWrapper?.public_getActionBar() ?: super.getActionBar()
+ }
+
+ override fun platform_getActionBar(): ActionBar? {
+ return super.getActionBar()
+ }
+
+ override fun platform_getApplication(): Application? {
+ return super.getApplication()
+ }
+
+ override fun getCaller(): ComponentCaller? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ activityProxyWrapper?.public_getCaller()
+ } else {
+ super.getCaller()
+ }
+ }
+
+ override fun platform_getCaller(): ComponentCaller? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ super.getCaller()
+ } else {
+ null
+ }
+ }
+
+ override fun getCallingActivity(): ComponentName? {
+ return activityProxyWrapper?.public_getCallingActivity() ?: super.getCallingActivity()
+ }
+
+ override fun platform_getCallingActivity(): ComponentName? {
+ return super.getCallingActivity()
+ }
+
+ override fun getCallingPackage(): String? {
+ return activityProxyWrapper?.public_getCallingPackage() ?: super.getCallingPackage()
+ }
+
+ override fun platform_getCallingPackage(): String? {
+ return super.getCallingPackage()
+ }
+
+ override fun getChangingConfigurations(): Int {
+ return activityProxyWrapper?.public_getChangingConfigurations() ?: super.getChangingConfigurations()
+ }
+
+ override fun platform_getChangingConfigurations(): Int {
+ return super.getChangingConfigurations()
+ }
+
+ override fun getComponentName(): ComponentName? {
+ return activityProxyWrapper?.public_getComponentName() ?: super.getComponentName()
+ }
+
+ override fun platform_getComponentName(): ComponentName? {
+ return super.getComponentName()
+ }
+
+ override fun getContentScene(): Scene? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activityProxyWrapper?.public_getContentScene() ?: super.getContentScene()
+ } else {
+ null
+ }
+ }
+
+ override fun platform_getContentScene(): Scene? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ super.getContentScene()
+ } else {
+ null
+ }
+ }
+
+ override fun getContentTransitionManager(): TransitionManager? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activityProxyWrapper?.public_getContentTransitionManager() ?: super.getContentTransitionManager()
+ } else {
+ null
+ }
+ }
+
+ override fun platform_getContentTransitionManager(): TransitionManager? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ super.getContentTransitionManager()
+ } else {
+ null
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun getCurrentCaller(): ComponentCaller {
+ return activityProxyWrapper?.public_getCurrentCaller()!!
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_getCurrentCaller(): ComponentCaller {
+ return super.getCurrentCaller()
+ }
+
+ override fun getCurrentFocus(): View? {
+ return activityProxyWrapper?.public_getCurrentFocus() ?: super.getCurrentFocus()
+ }
+
+ override fun platform_getCurrentFocus(): View? {
+ return super.getCurrentFocus()
+ }
+
+ override fun getFragmentManager(): FragmentManager? {
+ return activityProxyWrapper?.public_getFragmentManager() ?: super.getFragmentManager()
+ }
+
+ override fun platform_getFragmentManager(): FragmentManager? {
+ return super.getFragmentManager()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun getInitialCaller(): ComponentCaller {
+ return activityProxyWrapper?.public_getInitialCaller()!!
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_getInitialCaller(): ComponentCaller {
+ return super.getInitialCaller()
+ }
+
+ override fun getIntent(): Intent? {
+ return activityProxyWrapper?.public_getIntent() ?: super.getIntent()
+ }
+ override fun platform_getIntent(): Intent? {
+ return super.getIntent()
+ }
+
+ override fun getLastNonConfigurationInstance(): Any? {
+ return activityProxyWrapper?.public_getLastNonConfigurationInstance() ?: super.getLastNonConfigurationInstance()
+ }
+
+ override fun platform_getLastNonConfigurationInstance(): Any? {
+ return super.getLastNonConfigurationInstance()
+ }
+
+ override fun getLaunchedFromPackage(): String? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ activityProxyWrapper?.public_getLaunchedFromPackage() ?: super.getLaunchedFromPackage()
+ } else {
+ null
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun platform_getLaunchedFromPackage(): String? {
+ return super.getLaunchedFromPackage()
+ }
+
+ override fun getLaunchedFromUid(): Int {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ activityProxyWrapper?.public_getLaunchedFromUid() ?: super.getLaunchedFromUid()
+ } else {
+ 0
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun platform_getLaunchedFromUid(): Int {
+ return super.getLaunchedFromUid()
+ }
+
+ override fun getLayoutInflater(): LayoutInflater {
+ return activityProxyWrapper?.public_getLayoutInflater() ?: super.getLayoutInflater()
+ }
+
+ override fun platform_getLayoutInflater(): LayoutInflater {
+ return super.getLayoutInflater()
+ }
+
+ @Deprecated
+ override fun getLoaderManager(): LoaderManager? {
+ return activityProxyWrapper?.public_getLoaderManager() ?: super.getLoaderManager()
+ }
+
+ @Deprecated
+ override fun platform_getLoaderManager(): LoaderManager? {
+ return activityProxyWrapper?.public_getLoaderManager() ?: super.getLoaderManager()
+ }
+
+ override fun getLocalClassName(): String {
+ return activityProxyWrapper?.public_getLocalClassName() ?: super.getLocalClassName()
+ }
+
+ override fun platform_getLocalClassName(): String {
+ return super.getLocalClassName()
+ }
+
+ override fun getMaxNumPictureInPictureActions(): Int {
+ return activityProxyWrapper?.public_getMaxNumPictureInPictureActions() ?: super.getMaxNumPictureInPictureActions()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun platform_getMaxNumPictureInPictureActions(): Int {
+ return super.getMaxNumPictureInPictureActions()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_getMediaController(): MediaController {
+ return super.getMediaController()
+ }
+
+ override fun getMenuInflater(): MenuInflater {
+ return activityProxyWrapper?.public_getMenuInflater() ?: super.getMenuInflater()
+ }
+
+ override fun platform_getMenuInflater(): MenuInflater {
+ return super.getMenuInflater()
+ }
+
+ override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+ return activityProxyWrapper?.public_getOnBackInvokedDispatcher() ?: super.getOnBackInvokedDispatcher()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun platform_getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+ return super.getOnBackInvokedDispatcher()
+ }
+
+ @Deprecated
+ override fun platform_getParent(): Activity {
+ return super.getParent()
+ }
+
+ override fun getParentActivityIntent(): Intent? {
+ return activityProxyWrapper?.public_getParentActivityIntent() ?: super.getParentActivityIntent()
+ }
+
+ override fun platform_getParentActivityIntent(): Intent? {
+ return super.getParentActivityIntent()
+ }
+
+ override fun getPreferences(mode: Int): SharedPreferences? {
+ return activityProxyWrapper?.public_getPreferences(mode) ?: super.getPreferences(mode)
+ }
+
+ override fun platform_getPreferences(mode: Int): SharedPreferences? {
+ return super.getPreferences(mode)
+ }
+
+ override fun getReferrer(): Uri? {
+ return activityProxyWrapper?.public_getReferrer() ?: super.getReferrer()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
+ override fun platform_getReferrer(): Uri? {
+ return super.getReferrer()
+ }
+
+ override fun getRequestedOrientation(): Int {
+ return activityProxyWrapper?.public_getRequestedOrientation() ?: super.getRequestedOrientation()
+ }
+
+ override fun platform_getRequestedOrientation(): Int {
+ return super.getRequestedOrientation()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_getSearchEvent(): SearchEvent? {
+ return super.getSearchEvent()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ override fun platform_getSplashScreen(): SplashScreen {
+ return super.getSplashScreen()
+ }
+
+ override fun getTaskId(): Int {
+ return activityProxyWrapper?.public_getTaskId() ?: super.getTaskId()
+ }
+
+ override fun platform_getTaskId(): Int {
+ return super.getTaskId()
+ }
+
+ override fun platform_getTitle(): CharSequence? {
+ return super.getTitle()
+ }
+
+ override fun platform_getTitleColor(): Int {
+ return super.getTitleColor()
+ }
+
+ override fun getVoiceInteractor(): VoiceInteractor? {
+ return activityProxyWrapper?.public_getVoiceInteractor() ?: super.getVoiceInteractor()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_getVoiceInteractor(): VoiceInteractor? {
+ return super.getVoiceInteractor()
+ }
+
+ override fun platform_getVolumeControlStream(): Int {
+ return super.getVolumeControlStream()
+ }
+
+ override fun getWindow(): Window? {
+ return activityProxyWrapper?.public_getWindow() ?: super.getWindow()
+ }
+
+ override fun platform_getWindow(): Window? {
+ return super.getWindow()
+ }
+
+ override fun getWindowManager(): WindowManager? {
+ return activityProxyWrapper?.public_getWindowManager() ?: super.getWindowManager()
+ }
+
+ override fun platform_getWindowManager(): WindowManager? {
+ return super.getWindowManager()
+ }
+
+ override fun hasWindowFocus(): Boolean {
+ return activityProxyWrapper?.public_hasWindowFocus() ?: super.hasWindowFocus()
+ }
+
+ override fun platform_hasWindowFocus(): Boolean {
+ return super.hasWindowFocus()
+ }
+
+ override fun invalidateOptionsMenu() {
+ activityProxyWrapper?.public_invalidateOptionsMenu() ?: super.invalidateOptionsMenu()
+ }
+
+ override fun platform_invalidateOptionsMenu() {
+ super.invalidateOptionsMenu()
+ }
+
+ override fun isActivityTransitionRunning(): Boolean {
+ return activityProxyWrapper?.public_isActivityTransitionRunning() ?: super.isActivityTransitionRunning()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun platform_isActivityTransitionRunning(): Boolean {
+ return super.isActivityTransitionRunning()
+ }
+
+ @Deprecated
+ fun isBackgroundVisibleBehind(): Boolean {
+ throw UnsupportedOperationException("isBackgroundVisibleBehind is SystemApi")
+ }
+
+ override fun platform_isBackgroundVisibleBehind(): Boolean {
+ throw UnsupportedOperationException("isBackgroundVisibleBehind is SystemApi")
+ }
+
+ override fun isChangingConfigurations(): Boolean {
+ return activityProxyWrapper?.public_isChangingConfigurations() ?: super.isChangingConfigurations()
+ }
+
+ override fun platform_isChangingConfigurations(): Boolean {
+ return super.isChangingConfigurations()
+ }
+
+ override fun platform_isChild(): Boolean {
+ return super.isChild()
+ }
+
+ override fun platform_isDestroyed(): Boolean {
+ return super.isDestroyed()
+ }
+
+ override fun isFinishing(): Boolean {
+ return activityProxyWrapper?.public_isFinishing() ?: super.isFinishing()
+ }
+
+ override fun platform_isFinishing(): Boolean {
+ return super.isFinishing()
+ }
+
+ override fun isImmersive(): Boolean {
+ return activityProxyWrapper?.public_isImmersive() ?: super.isImmersive()
+ }
+
+ override fun platform_isImmersive(): Boolean {
+ return super.isImmersive()
+ }
+
+ override fun isInMultiWindowMode(): Boolean {
+ return activityProxyWrapper?.public_isInMultiWindowMode() ?: super.isInMultiWindowMode()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_isInMultiWindowMode(): Boolean {
+ return super.isInMultiWindowMode()
+ }
+
+ override fun isInPictureInPictureMode(): Boolean {
+ return activityProxyWrapper?.public_isInPictureInPictureMode() ?: super.isInPictureInPictureMode()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_isInPictureInPictureMode(): Boolean {
+ return super.isInPictureInPictureMode()
+ }
+
+ override fun isLaunchedFromBubble(): Boolean {
+ return activityProxyWrapper?.public_isLaunchedFromBubble() ?: super.isLaunchedFromBubble()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ override fun platform_isLaunchedFromBubble(): Boolean {
+ return super.isLaunchedFromBubble()
+ }
+
+ override fun isLocalVoiceInteractionSupported(): Boolean {
+ return activityProxyWrapper?.public_isLocalVoiceInteractionSupported() ?: super.isLocalVoiceInteractionSupported()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_isLocalVoiceInteractionSupported(): Boolean {
+ return super.isLocalVoiceInteractionSupported()
+ }
+
+ override fun isTaskRoot(): Boolean {
+ return activityProxyWrapper?.public_isTaskRoot() ?: super.isTaskRoot()
+ }
+ override fun platform_isTaskRoot(): Boolean {
+ return super.isTaskRoot()
+ }
+
+ override fun isVoiceInteraction(): Boolean {
+ return activityProxyWrapper?.public_isVoiceInteraction() ?: super.isVoiceInteraction()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_isVoiceInteraction(): Boolean {
+ return super.isVoiceInteraction()
+ }
+ override fun isVoiceInteractionRoot(): Boolean {
+ return activityProxyWrapper?.public_isVoiceInteractionRoot() ?: super.isVoiceInteractionRoot()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_isVoiceInteractionRoot(): Boolean {
+ return super.isVoiceInteractionRoot()
+ }
+
+ @Deprecated
+ override fun platform_managedQuery(uri: Uri?, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
+ return super.managedQuery(uri, projection, selection, selectionArgs, sortOrder)
+ }
+
+ override fun moveTaskToBack(nonRoot: Boolean): Boolean {
+ return activityProxyWrapper?.public_moveTaskToBack(nonRoot) ?: super.moveTaskToBack(nonRoot)
+ }
+
+ override fun platform_moveTaskToBack(nonRoot: Boolean): Boolean {
+ return super.moveTaskToBack(nonRoot)
+ }
+
+ override fun navigateUpTo(intent: Intent?): Boolean {
+ return activityProxyWrapper?.public_navigateUpTo(intent) ?: super.navigateUpTo(intent)
+ }
+
+ override fun platform_navigateUpTo(intent: Intent?): Boolean {
+ return super.navigateUpTo(intent)
+ }
+
+ override fun navigateUpToFromChild(child: Activity?, intent: Intent?): Boolean {
+ return activityProxyWrapper?.public_navigateUpToFromChild(child, intent) ?: super.navigateUpToFromChild(child, intent)
+ }
+
+ override fun platform_navigateUpToFromChild(child: Activity?, intent: Intent?): Boolean {
+ return super.navigateUpToFromChild(child, intent)
+ }
+
+ override fun onActionModeFinished(mode: ActionMode?) {
+ activityProxyWrapper?.public_onActionModeFinished(mode) ?: super.onActionModeFinished(mode)
+ }
+
+ override fun platform_onActionModeFinished(mode: ActionMode?) {
+ super.onActionModeFinished(mode)
+ }
+
+ override fun onActionModeStarted(mode: ActionMode?) {
+ activityProxyWrapper?.public_onActionModeStarted(mode) ?: super.onActionModeStarted(mode)
+ }
+
+ override fun platform_onActionModeStarted(mode: ActionMode?) {
+ super.onActionModeStarted(mode)
+ }
+
+ override fun onActivityReenter(resultCode: Int, data: Intent?) {
+ activityProxyWrapper?.public_onActivityReenter(resultCode, data) ?: super.onActivityReenter(resultCode, data)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_onActivityReenter(resultCode: Int, data: Intent?) {
+ super.onActivityReenter(resultCode, data)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ activityProxyWrapper?.public_onActivityResult(requestCode, resultCode, data) ?: super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ override fun platform_onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, caller: ComponentCaller) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ activityProxyWrapper?.public_onActivityResult(requestCode, resultCode, data, caller) ?: super.onActivityResult(requestCode, resultCode, data, caller)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, caller: ComponentCaller) {
+ super.onActivityResult(requestCode, resultCode, data, caller)
+ }
+
+ @Deprecated
+ override fun onAttachFragment(fragment: Fragment?) {
+ activityProxyWrapper?.public_onAttachFragment(fragment) ?: super.onAttachFragment(fragment)
+ }
+
+ @Deprecated
+ override fun platform_onAttachFragment(fragment: Fragment?) {
+ super.onAttachFragment(fragment)
+ }
+
+ override fun onAttachedToWindow() {
+ activityProxyWrapper?.public_onAttachedToWindow() ?: super.onAttachedToWindow()
+ }
+
+ override fun platform_onAttachedToWindow() {
+ super.onAttachedToWindow()
+ }
+
+ @Deprecated
+ override fun onBackPressed() {
+ activityProxyWrapper?.public_onBackPressed() ?: super.onBackPressed()
+ }
+
+ override fun platform_onBackPressed() {
+ super.onBackPressed()
+ }
+
+ @Deprecated
+ fun onBackgroundVisibleBehindChanged(visible: Boolean) {
+ throw UnsupportedOperationException("not supported")
+ }
+
+ @Deprecated
+ override fun platform_onBackgroundVisibleBehindChanged(visible: Boolean) {
+ throw UnsupportedOperationException("not supported")
+ }
+
+ override fun onChildTitleChanged(child: Activity?, title: CharSequence?) {
+ activityProxyWrapper?.public_onChildTitleChanged(child, title) ?: super.onChildTitleChanged(child, title)
+ }
+
+ override fun platform_onChildTitleChanged(child: Activity?, title: CharSequence?) {
+ super.onChildTitleChanged(child, title)
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ activityProxyWrapper?.public_onConfigurationChanged(newConfig) ?: super.onConfigurationChanged(newConfig)
+ }
+
+ override fun platform_onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ }
+
+ override fun onContentChanged() {
+ activityProxyWrapper?.public_onContentChanged() ?: super.onContentChanged()
+ }
+
+ override fun platform_onContentChanged() {
+ super.onContentChanged()
+ }
+
+ override fun onContextItemSelected(item: MenuItem): Boolean {
+ return activityProxyWrapper?.public_onContextItemSelected(item) ?: super.onContextItemSelected(item)
+ }
+
+ override fun platform_onContextItemSelected(item: MenuItem): Boolean {
+ return super.onContextItemSelected(item)
+ }
+
+ override fun onContextMenuClosed(menu: Menu) {
+ activityProxyWrapper?.public_onContextMenuClosed(menu) ?: super.onContextMenuClosed(menu)
+ }
+
+ override fun platform_onContextMenuClosed(menu: Menu) {
+ super.onContextMenuClosed(menu)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ activityProxyWrapper?.public_onCreate(savedInstanceState) ?: super.onCreate(savedInstanceState)
+ }
+
+ override fun platform_onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ }
+ override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
+ activityProxyWrapper?.public_onCreate(savedInstanceState, persistentState) ?: super.onCreate(savedInstanceState, persistentState)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
+ super.onCreate(savedInstanceState, persistentState)
+ }
+
+ override fun onCreateContextMenu(menu: ContextMenu?, view: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
+ activityProxyWrapper?.public_onCreateContextMenu(menu, view, menuInfo)
+ ?: super.onCreateContextMenu(menu, view, menuInfo)
+ }
+
+ override fun platform_onCreateContextMenu(menu: ContextMenu?, view: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
+ super.onCreateContextMenu(menu, view, menuInfo)
+ }
+
+ override fun onCreateDescription(): CharSequence? {
+ return activityProxyWrapper?.public_onCreateDescription() ?: super.onCreateDescription()
+ }
+
+ override fun platform_onCreateDescription(): CharSequence? {
+ return super.onCreateDescription()
+ }
+
+ override fun onCreateNavigateUpTaskStack(builder: TaskStackBuilder?) {
+ activityProxyWrapper?.public_onCreateNavigateUpTaskStack(builder) ?: super.onCreateNavigateUpTaskStack(builder)
+ }
+
+ override fun platform_onCreateNavigateUpTaskStack(builder: TaskStackBuilder?) {
+ super.onCreateNavigateUpTaskStack(builder)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ return activityProxyWrapper?.public_onCreateOptionsMenu(menu) ?: super.onCreateOptionsMenu(menu)
+ }
+
+ override fun platform_onCreateOptionsMenu(menu: Menu): Boolean {
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onCreatePanelMenu(featureId: Int, menu: Menu): Boolean {
+ return activityProxyWrapper?.public_onCreatePanelMenu(featureId, menu) ?: super.onCreatePanelMenu(featureId, menu)
+ }
+
+ override fun platform_onCreatePanelMenu(featureId: Int, menu: Menu): Boolean {
+ return super.onCreatePanelMenu(featureId, menu)
+ }
+
+ override fun onCreatePanelView(featureId: Int): View? {
+ return activityProxyWrapper?.public_onCreatePanelView(featureId) ?: super.onCreatePanelView(featureId)
+ }
+
+ override fun platform_onCreatePanelView(featureId: Int): View? {
+ return super.onCreatePanelView(featureId)
+ }
+
+ @Deprecated
+ override fun onCreateThumbnail(outBitmap: Bitmap?, canvas: Canvas?): Boolean {
+ return activityProxyWrapper?.public_onCreateThumbnail(outBitmap, canvas) ?: super.onCreateThumbnail(outBitmap, canvas)
+ }
+
+ @Deprecated
+ override fun platform_onCreateThumbnail(outBitmap: Bitmap?, canvas: Canvas?): Boolean {
+ return super.onCreateThumbnail(outBitmap, canvas)
+ }
+
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
+ return activityProxyWrapper?.public_onCreateView(parent, name, context, attrs) ?: super.onCreateView(parent, name, context, attrs)
+ }
+
+ override fun platform_onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
+ return super.onCreateView(parent, name, context, attrs)
+ }
+
+ override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ return activityProxyWrapper?.public_onCreateView(name, context, attrs) ?: super.onCreateView(name, context, attrs)
+ }
+
+ override fun platform_onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ return super.onCreateView(name, context, attrs)
+ }
+
+ override fun onDestroy() {
+ activityProxyWrapper?.public_onDestroy() ?: super.onDestroy()
+ }
+
+ override fun platform_onDestroy() {
+ super.onDestroy()
+ }
+
+ override fun onDetachedFromWindow() {
+ activityProxyWrapper?.public_onDetachedFromWindow() ?: super.onDetachedFromWindow()
+ }
+
+ override fun platform_onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ }
+
+ override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
+ return activityProxyWrapper?.public_onGenericMotionEvent(event) ?: super.onGenericMotionEvent(event)
+ }
+
+ override fun platform_onGenericMotionEvent(event: MotionEvent?): Boolean {
+ return super.onGenericMotionEvent(event)
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ return activityProxyWrapper?.public_onKeyDown(keyCode, event) ?: super.onKeyDown(keyCode, event)
+ }
+
+ override fun platform_onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ return super.onKeyDown(keyCode, event)
+ }
+
+ override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
+ return activityProxyWrapper?.public_onKeyLongPress(keyCode, event) ?: super.onKeyLongPress(keyCode, event)
+ }
+
+ override fun platform_onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
+ return super.onKeyLongPress(keyCode, event)
+ }
+
+ override fun onKeyMultiple(keyCode: Int, repeatCount: Int, event: KeyEvent?): Boolean {
+ return activityProxyWrapper?.public_onKeyMultiple(keyCode, repeatCount, event) ?: super.onKeyMultiple(keyCode, repeatCount, event)
+ }
+
+ override fun platform_onKeyMultiple(keyCode: Int, repeatCount: Int, event: KeyEvent?): Boolean {
+ return super.onKeyMultiple(keyCode, repeatCount, event)
+ }
+
+ override fun onKeyShortcut(keyCode: Int, event: KeyEvent?): Boolean {
+ return activityProxyWrapper?.public_onKeyShortcut(keyCode, event) ?: super.onKeyShortcut(keyCode, event)
+ }
+
+ override fun platform_onKeyShortcut(keyCode: Int, event: KeyEvent?): Boolean {
+ return super.onKeyShortcut(keyCode, event)
+ }
+
+ override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
+ return activityProxyWrapper?.public_onKeyUp(keyCode, event) ?: super.onKeyUp(keyCode, event)
+ }
+
+ override fun platform_onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
+ return super.onKeyUp(keyCode, event)
+ }
+
+ override fun onLocalVoiceInteractionStarted() {
+ activityProxyWrapper?.public_onLocalVoiceInteractionStarted() ?: super.onLocalVoiceInteractionStarted()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_onLocalVoiceInteractionStarted() {
+ super.onLocalVoiceInteractionStarted()
+ }
+
+ override fun onLocalVoiceInteractionStopped() {
+ activityProxyWrapper?.public_onLocalVoiceInteractionStopped() ?: super.onLocalVoiceInteractionStopped()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_onLocalVoiceInteractionStopped() {
+ super.onLocalVoiceInteractionStopped()
+ }
+
+ override fun onMenuItemSelected(featureId: Int, item: MenuItem): Boolean {
+ return activityProxyWrapper?.public_onMenuItemSelected(featureId, item) ?: super.onMenuItemSelected(featureId, item)
+ }
+
+ override fun platform_onMenuItemSelected(featureId: Int, item: MenuItem): Boolean {
+ return super.onMenuItemSelected(featureId, item)
+ }
+
+ override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
+ return activityProxyWrapper?.public_onMenuOpened(featureId, menu) ?: super.onMenuOpened(featureId, menu)
+ }
+
+ override fun platform_onMenuOpened(featureId: Int, menu: Menu): Boolean {
+ return super.onMenuOpened(featureId, menu)
+ }
+
+ override fun onNavigateUp(): Boolean {
+ return activityProxyWrapper?.public_onNavigateUp() ?: super.onNavigateUp()
+ }
+
+ override fun platform_onNavigateUp(): Boolean {
+ return super.onNavigateUp()
+ }
+
+ @Deprecated
+ override fun onNavigateUpFromChild(child: Activity?): Boolean {
+ return activityProxyWrapper?.public_onNavigateUpFromChild(child) ?: super.onNavigateUpFromChild(child)
+ }
+
+ @Deprecated
+ override fun platform_onNavigateUpFromChild(child: Activity?): Boolean {
+ return super.onNavigateUpFromChild(child)
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ activityProxyWrapper?.public_onNewIntent(intent) ?: super.onNewIntent(intent)
+ }
+
+ override fun platform_onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
+ activityProxyWrapper?.public_onNewIntent(intent, caller) ?: super.onNewIntent(intent, caller)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_onNewIntent(intent: Intent, caller: ComponentCaller) {
+ super.onNewIntent(intent, caller)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return activityProxyWrapper?.public_onOptionsItemSelected(item) ?: super.onOptionsItemSelected(item)
+ }
+
+ override fun platform_onOptionsItemSelected(item: MenuItem): Boolean {
+ return super.onOptionsItemSelected(item)
+ }
+ override fun onOptionsMenuClosed(menu: Menu) {
+ activityProxyWrapper?.public_onOptionsMenuClosed(menu) ?: super.onOptionsMenuClosed(menu)
+ }
+
+ override fun platform_onOptionsMenuClosed(menu: Menu) {
+ return super.onOptionsMenuClosed(menu)
+ }
+
+ override fun onPanelClosed(featureId: Int, menu: Menu) {
+ activityProxyWrapper?.public_onPanelClosed(featureId, menu) ?: super.onPanelClosed(featureId, menu)
+ }
+
+ override fun platform_onPanelClosed(featureId: Int, menu: Menu) {
+ return super.onPanelClosed(featureId, menu)
+ }
+
+ override fun onPause() {
+ activityProxyWrapper?.public_onPause() ?: super.onPause()
+ }
+
+ override fun platform_onPause() {
+ return super.onPause()
+ }
+
+ override fun onPointerCaptureChanged(hasCapture: Boolean) {
+ activityProxyWrapper?.public_onPointerCaptureChanged(hasCapture) ?: super.onPointerCaptureChanged(hasCapture)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun platform_onPointerCaptureChanged(hasCapture: Boolean) {
+ super.onPointerCaptureChanged(hasCapture)
+ }
+ override fun onPostCreate(savedInstanceState: Bundle?) {
+ activityProxyWrapper?.public_onPostCreate(savedInstanceState) ?: super.onPostCreate(savedInstanceState)
+ }
+
+ override fun platform_onPostCreate(savedInstanceState: Bundle?) {
+ return super.onPostCreate(savedInstanceState)
+ }
+
+ override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
+ activityProxyWrapper?.public_onPostCreate(savedInstanceState, persistentState) ?: super.onPostCreate(savedInstanceState, persistentState)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
+ return super.onPostCreate(savedInstanceState, persistentState)
+ }
+
+ override fun onPostResume() {
+ activityProxyWrapper?.public_onPostResume() ?: super.onPostResume()
+ }
+
+ override fun platform_onPostResume() {
+ return super.onPostResume()
+ }
+
+ override fun onPrepareNavigateUpTaskStack(builder: TaskStackBuilder?) {
+ activityProxyWrapper?.public_onPrepareNavigateUpTaskStack(builder) ?: super.onPrepareNavigateUpTaskStack(builder)
+ }
+
+ override fun platform_onPrepareNavigateUpTaskStack(builder: TaskStackBuilder?) {
+ super.onPrepareNavigateUpTaskStack(builder)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ return activityProxyWrapper?.public_onPrepareOptionsMenu(menu) ?: super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun platform_onPrepareOptionsMenu(menu: Menu): Boolean {
+ return super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onPreparePanel(featureId: Int, view: View?, menu: Menu): Boolean {
+ return activityProxyWrapper?.public_onPreparePanel(featureId, view, menu) ?: super.onPreparePanel(featureId, view, menu)
+ }
+
+ override fun platform_onPreparePanel(featureId: Int, view: View?, menu: Menu): Boolean {
+ return super.onPreparePanel(featureId, view, menu)
+ }
+
+ override fun onProvideReferrer(): Uri? {
+ return activityProxyWrapper?.public_onProvideReferrer() ?: super.onProvideReferrer()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_onProvideReferrer(): Uri? {
+ return super.onProvideReferrer()
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ activityProxyWrapper?.public_onRequestPermissionsResult(requestCode, permissions, grantResults) ?: super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray, userId: Int) {
+ activityProxyWrapper?.public_onRequestPermissionsResult(requestCode, permissions, grantResults, userId) ?: super.onRequestPermissionsResult(requestCode, permissions, grantResults, userId)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray, userId: Int) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults, userId)
+ }
+
+ override fun onRestart() {
+ activityProxyWrapper?.public_onRestart() ?: super.onRestart()
+ }
+
+ override fun platform_onRestart() {
+ super.onRestart()
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ activityProxyWrapper?.public_onRestoreInstanceState(savedInstanceState) ?: super.onRestoreInstanceState(savedInstanceState)
+ }
+
+ override fun platform_onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
+ activityProxyWrapper?.public_onRestoreInstanceState(savedInstanceState, persistentState) ?: super.onRestoreInstanceState(savedInstanceState, persistentState)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_onRestoreInstanceState(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
+ super.onRestoreInstanceState(savedInstanceState, persistentState)
+ }
+
+ override fun onResume() {
+ activityProxyWrapper?.public_onResume() ?: super.onResume()
+ }
+
+ override fun platform_onResume() {
+ super.onResume()
+ }
+
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ activityProxyWrapper?.public_onSaveInstanceState(outState) ?: super.onSaveInstanceState(outState)
+ }
+
+ override fun platform_onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
+ activityProxyWrapper?.public_onSaveInstanceState(outState, outPersistentState) ?: super.onSaveInstanceState(outState, outPersistentState)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
+ super.onSaveInstanceState(outState, outPersistentState)
+ }
+ override fun onSearchRequested(): Boolean {
+ return activityProxyWrapper?.public_onSearchRequested() ?: super.onSearchRequested()
+ }
+
+ override fun platform_onSearchRequested(): Boolean {
+ return super.onSearchRequested()
+ }
+
+ override fun onSearchRequested(event: SearchEvent?): Boolean {
+ return activityProxyWrapper?.public_onSearchRequested(event) ?: super.onSearchRequested(event)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_onSearchRequested(event: SearchEvent?): Boolean {
+ return super.onSearchRequested(event)
+ }
+
+ override fun onStart() {
+ activityProxyWrapper?.public_onStart() ?: super.onStart()
+ }
+
+ override fun platform_onStart() {
+ super.onStart()
+ }
+
+ @Deprecated
+ override fun onStateNotSaved() {
+ activityProxyWrapper?.public_onStateNotSaved() ?: super.onStateNotSaved()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ @Deprecated
+ override fun platform_onStateNotSaved() {
+ super.onStateNotSaved()
+ }
+
+ override fun onStop() {
+ activityProxyWrapper?.public_onStop() ?: super.onStop()
+ }
+
+ override fun platform_onStop() {
+ super.onStop()
+ }
+
+ override fun onTitleChanged(title: CharSequence?, color: Int) {
+ activityProxyWrapper?.public_onTitleChanged(title, color) ?: super.onTitleChanged(title, color)
+ }
+
+ override fun platform_onTitleChanged(title: CharSequence?, color: Int) {
+ super.onTitleChanged(title, color)
+ }
+
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ return activityProxyWrapper?.public_onTouchEvent(event) ?: super.onTouchEvent(event)
+ }
+
+ override fun platform_onTouchEvent(event: MotionEvent?): Boolean {
+ return super.onTouchEvent(event)
+ }
+
+ override fun onTrackballEvent(event: MotionEvent?): Boolean {
+ return activityProxyWrapper?.public_onTrackballEvent(event) ?: super.onTrackballEvent(event)
+ }
+
+ override fun platform_onTrackballEvent(event: MotionEvent?): Boolean {
+ return super.onTrackballEvent(event)
+ }
+
+
+ override fun onUserInteraction() {
+ activityProxyWrapper?.public_onUserInteraction() ?: super.onUserInteraction()
+ }
+
+ override fun platform_onUserInteraction() {
+ super.onUserInteraction()
+ }
+
+ override fun onUserLeaveHint() {
+ activityProxyWrapper?.public_onUserLeaveHint() ?: super.onUserLeaveHint()
+ }
+
+ override fun platform_onUserLeaveHint() {
+ super.onUserLeaveHint()
+ }
+
+ @Deprecated
+ override fun onVisibleBehindCanceled() {
+ activityProxyWrapper?.public_onVisibleBehindCanceled() ?: super.onVisibleBehindCanceled()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ @Deprecated
+ override fun platform_onVisibleBehindCanceled() {
+ super.onVisibleBehindCanceled()
+ }
+
+ override fun onWindowAttributesChanged(params: WindowManager.LayoutParams?) {
+ activityProxyWrapper?.public_onWindowAttributesChanged(params) ?: super.onWindowAttributesChanged(params)
+ }
+
+ override fun platform_onWindowAttributesChanged(params: WindowManager.LayoutParams?) {
+ super.onWindowAttributesChanged(params)
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ activityProxyWrapper?.public_onWindowFocusChanged(hasFocus) ?: super.onWindowFocusChanged(hasFocus)
+ }
+
+ override fun platform_onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ }
+
+ override fun onWindowStartingActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return activityProxyWrapper?.public_onWindowStartingActionMode(callback) ?: super.onWindowStartingActionMode(callback)
+ }
+
+ override fun platform_onWindowStartingActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return super.onWindowStartingActionMode(callback)
+ }
+
+ override fun onWindowStartingActionMode(callback: ActionMode.Callback?, type: Int): ActionMode? {
+ return activityProxyWrapper?.public_onWindowStartingActionMode(callback, type) ?: super.onWindowStartingActionMode(callback, type)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_onWindowStartingActionMode(callback: ActionMode.Callback?, type: Int): ActionMode? {
+ return super.onWindowStartingActionMode(callback, type)
+ }
+
+ override fun openContextMenu(view: View?) {
+ activityProxyWrapper?.public_openContextMenu(view) ?: super.openContextMenu(view)
+ }
+
+ override fun platform_openContextMenu(view: View?) {
+ super.openContextMenu(view)
+ }
+
+ override fun openOptionsMenu() {
+ activityProxyWrapper?.public_openOptionsMenu() ?: super.openOptionsMenu()
+ }
+
+ override fun platform_openOptionsMenu() {
+ super.openOptionsMenu()
+ }
+
+ override fun overrideActivityTransition(transitionType: Int, enterAnim: Int, exitAnim: Int) {
+ activityProxyWrapper?.public_overrideActivityTransition(transitionType, enterAnim, exitAnim) ?: super.overrideActivityTransition(transitionType, enterAnim, exitAnim)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun platform_overrideActivityTransition(transitionType: Int, enterAnim: Int, exitAnim: Int) {
+ super.overrideActivityTransition(transitionType, enterAnim, exitAnim)
+ }
+
+ override fun overrideActivityTransition(transitionType: Int, enterAnim: Int, exitAnim: Int, backgroundColor: Int) {
+ activityProxyWrapper?.public_overrideActivityTransition(transitionType, enterAnim, exitAnim, backgroundColor) ?: super.overrideActivityTransition(transitionType, enterAnim, exitAnim, backgroundColor)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun platform_overrideActivityTransition(transitionType: Int, enterAnim: Int, exitAnim: Int, backgroundColor: Int) {
+ super.overrideActivityTransition(transitionType, enterAnim, exitAnim, backgroundColor)
+ }
+
+ @Deprecated
+ override fun overridePendingTransition(enterAnim: Int, exitAnim: Int) {
+ activityProxyWrapper?.public_overridePendingTransition(enterAnim, exitAnim) ?: super.overridePendingTransition(enterAnim, exitAnim)
+ }
+
+ @Deprecated
+ override fun platform_overridePendingTransition(enterAnim: Int, exitAnim: Int) {
+ super.overridePendingTransition(enterAnim, exitAnim)
+ }
+
+ @Deprecated
+ override fun overridePendingTransition(enterAnim: Int, exitAnim: Int, backgroundColor: Int) {
+ activityProxyWrapper?.public_overridePendingTransition(enterAnim, exitAnim, backgroundColor) ?: super.overridePendingTransition(enterAnim, exitAnim, backgroundColor)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Deprecated
+ override fun platform_overridePendingTransition(enterAnim: Int, exitAnim: Int, backgroundColor: Int) {
+ super.overridePendingTransition(enterAnim, exitAnim, backgroundColor)
+ }
+
+ override fun postponeEnterTransition() {
+ activityProxyWrapper?.public_postponeEnterTransition() ?: super.postponeEnterTransition()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_postponeEnterTransition() {
+ super.postponeEnterTransition()
+ }
+
+ override fun recreate() {
+ activityProxyWrapper?.public_recreate() ?: super.recreate()
+ }
+
+ override fun platform_recreate() {
+ super.recreate()
+ }
+
+ override fun registerActivityLifecycleCallbacks(callback: Application.ActivityLifecycleCallbacks) {
+ activityProxyWrapper?.public_registerActivityLifecycleCallbacks(callback) ?: super.registerActivityLifecycleCallbacks(callback)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ override fun platform_registerActivityLifecycleCallbacks(callback: Application.ActivityLifecycleCallbacks) {
+ super.registerActivityLifecycleCallbacks(callback)
+ }
+
+ override fun registerForContextMenu(view: View?) {
+ activityProxyWrapper?.public_registerForContextMenu(view) ?: super.registerForContextMenu(view)
+ }
+
+ override fun platform_registerForContextMenu(view: View?) {
+ super.registerForContextMenu(view)
+ }
+
+ override fun releaseInstance(): Boolean {
+ return activityProxyWrapper?.public_releaseInstance() ?: super.releaseInstance()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_releaseInstance(): Boolean {
+ return super.releaseInstance()
+ }
+
+ override fun reportFullyDrawn() {
+ activityProxyWrapper?.public_reportFullyDrawn() ?: super.reportFullyDrawn()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_reportFullyDrawn() {
+ super.reportFullyDrawn()
+ }
+
+ override fun requestDragAndDropPermissions(event: DragEvent?): DragAndDropPermissions? {
+ return activityProxyWrapper?.public_requestDragAndDropPermissions(event) ?: super.requestDragAndDropPermissions(event)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_requestDragAndDropPermissions(event: DragEvent?): DragAndDropPermissions? {
+ return super.requestDragAndDropPermissions(event)
+ }
+
+ override fun requestFullscreenMode(request: Int, approvalCallback: OutcomeReceiver?) {
+ activityProxyWrapper?.public_requestFullscreenMode(request, approvalCallback) ?: super.requestFullscreenMode(request, approvalCallback)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun platform_requestFullscreenMode(request: Int, approvalCallback: OutcomeReceiver?) {
+ super.requestFullscreenMode(request, approvalCallback)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_requestPermissions(permissions: Array, requestCode: Int) {
+ super.requestPermissions(permissions, requestCode)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_requestPermissions(permissions: Array, requestCode: Int, userId: Int) {
+ super.requestPermissions(permissions, requestCode, userId)
+ }
+
+ @Deprecated
+ override fun requestVisibleBehind(visible: Boolean): Boolean {
+ return activityProxyWrapper?.public_requestVisibleBehind(visible) ?: super.requestVisibleBehind(visible)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ @Deprecated
+ override fun platform_requestVisibleBehind(visible: Boolean): Boolean {
+ return super.requestVisibleBehind(visible)
+ }
+
+ override fun platform_requestWindowFeature(featureId: Int): Boolean {
+ return super.requestWindowFeature(featureId)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.P)
+ override fun platform_requireViewById(id: Int): View? {
+ return super.requireViewById(id)
+ }
+
+ override fun platform_runOnUiThread(action: Runnable?) {
+ super.runOnUiThread(action)
+ }
+
+ override fun setActionBar(toolbar: Toolbar?) {
+ activityProxyWrapper?.public_setActionBar(toolbar) ?: super.setActionBar(toolbar)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_setActionBar(toolbar: Toolbar?) {
+ super.setActionBar(toolbar)
+ }
+
+ override fun setAllowCrossUidActivitySwitchFromBelow(allow: Boolean) {
+ activityProxyWrapper?.public_setAllowCrossUidActivitySwitchFromBelow(allow) ?: super.setAllowCrossUidActivitySwitchFromBelow(allow)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_setAllowCrossUidActivitySwitchFromBelow(allow: Boolean) {
+ super.setAllowCrossUidActivitySwitchFromBelow(allow)
+ }
+
+ override fun setContentTransitionManager(transitionManager: TransitionManager?) {
+ activityProxyWrapper?.public_setContentTransitionManager(transitionManager) ?: super.setContentTransitionManager(transitionManager)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_setContentTransitionManager(transitionManager: TransitionManager?) {
+ super.setContentTransitionManager(transitionManager)
+ }
+
+ override fun setContentView(layoutResID: Int) {
+ activityProxyWrapper?.public_setContentView(layoutResID) ?: super.setContentView(layoutResID)
+ }
+
+ override fun platform_setContentView(layoutResID: Int) {
+ super.setContentView(layoutResID)
+ }
+
+ override fun setContentView(view: View?) {
+ activityProxyWrapper?.public_setContentView(view) ?: super.setContentView(view)
+ }
+
+ override fun platform_setContentView(view: View?) {
+ super.setContentView(view)
+ }
+
+ override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ activityProxyWrapper?.public_setContentView(view, params) ?: super.setContentView(view, params)
+ }
+
+ override fun platform_setContentView(view: View?, params: ViewGroup.LayoutParams?) {
+ super.setContentView(view, params)
+ }
+
+ override fun platform_setDefaultKeyMode(mode: Int) {
+ super.setDefaultKeyMode(mode)
+ }
+ override fun setEnterSharedElementCallback(callback: SharedElementCallback?) {
+ activityProxyWrapper?.public_setEnterSharedElementCallback(callback) ?: super.setEnterSharedElementCallback(callback)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_setEnterSharedElementCallback(callback: SharedElementCallback?) {
+ super.setEnterSharedElementCallback(callback)
+ }
+
+ override fun setExitSharedElementCallback(callback: SharedElementCallback?) {
+ activityProxyWrapper?.public_setExitSharedElementCallback(callback) ?: super.setExitSharedElementCallback(callback)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_setExitSharedElementCallback(callback: SharedElementCallback?) {
+ super.setExitSharedElementCallback(callback)
+ }
+
+ override fun platform_setFeatureDrawable(featureId: Int, drawable: Drawable?) {
+ super.setFeatureDrawable(featureId, drawable)
+ }
+
+ override fun platform_setFeatureDrawableAlpha(featureId: Int, alpha: Int) {
+ super.setFeatureDrawableAlpha(featureId, alpha)
+ }
+
+ override fun platform_setFeatureDrawableResource(featureId: Int, resId: Int) {
+ super.setFeatureDrawableResource(featureId, resId)
+ }
+
+ override fun platform_setFeatureDrawableUri(featureId: Int, uri: Uri?) {
+ super.setFeatureDrawableUri(featureId, uri)
+ }
+
+ override fun setFinishOnTouchOutside(finish: Boolean) {
+ activityProxyWrapper?.public_setFinishOnTouchOutside(finish) ?: super.setFinishOnTouchOutside(finish)
+ }
+
+ override fun platform_setFinishOnTouchOutside(finish: Boolean) {
+ super.setFinishOnTouchOutside(finish)
+ }
+
+ override fun setImmersive(immersive: Boolean) {
+ activityProxyWrapper?.public_setImmersive(immersive) ?: super.setImmersive(immersive)
+ }
+
+ override fun platform_setImmersive(immersive: Boolean) {
+ super.setImmersive(immersive)
+ }
+
+ override fun setInheritShowWhenLocked(showWhenLocked: Boolean) {
+ activityProxyWrapper?.public_setInheritShowWhenLocked(showWhenLocked) ?: super.setInheritShowWhenLocked(showWhenLocked)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ override fun platform_setInheritShowWhenLocked(showWhenLocked: Boolean) {
+ super.setInheritShowWhenLocked(showWhenLocked)
+ }
+
+ override fun setIntent(intent: Intent?) {
+ activityProxyWrapper?.public_setIntent(intent) ?: super.setIntent(intent)
+ }
+
+ override fun platform_setIntent(intent: Intent?) {
+ super.setIntent(intent)
+ }
+
+ override fun setIntent(intent: Intent?, caller: ComponentCaller?) {
+ activityProxyWrapper?.public_setIntent(intent, caller) ?: super.setIntent(intent, caller)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_setIntent(intent: Intent?, caller: ComponentCaller?) {
+ super.setIntent(intent, caller)
+ }
+
+ override fun setLocusContext(locusId: LocusId?, bundle: Bundle?) {
+ activityProxyWrapper?.public_setLocusContext(locusId, bundle) ?: super.setLocusContext(locusId, bundle)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ override fun platform_setLocusContext(locusId: LocusId?, bundle: Bundle?) {
+ super.setLocusContext(locusId, bundle)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_setMediaController(controller: MediaController?) {
+ super.setMediaController(controller)
+ }
+
+ override fun setPictureInPictureParams(params: PictureInPictureParams) {
+ activityProxyWrapper?.public_setPictureInPictureParams(params) ?: super.setPictureInPictureParams(params)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun platform_setPictureInPictureParams(params: PictureInPictureParams) {
+ super.setPictureInPictureParams(params)
+ }
+
+ @Deprecated
+ override fun platform_setProgress(progress: Int) {
+ super.setProgress(progress)
+ }
+
+ @Deprecated
+ override fun platform_setProgressBarIndeterminate(indeterminate: Boolean) {
+ super.setProgressBarIndeterminate(indeterminate)
+ }
+
+ @Deprecated
+ override fun platform_setProgressBarIndeterminateVisibility(visible: Boolean) {
+ super.setProgressBarIndeterminateVisibility(visible)
+ }
+
+ override fun platform_setProgressBarVisibility(visible: Boolean) {
+ super.setProgressBarVisibility(visible)
+ }
+
+ override fun setRecentsScreenshotEnabled(enabled: Boolean) {
+ activityProxyWrapper?.public_setRecentsScreenshotEnabled(enabled) ?: super.setRecentsScreenshotEnabled(enabled)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun platform_setRecentsScreenshotEnabled(enabled: Boolean) {
+ super.setRecentsScreenshotEnabled(enabled)
+ }
+
+ override fun setRequestedOrientation(orientation: Int) {
+ activityProxyWrapper?.public_setRequestedOrientation(orientation) ?: super.setRequestedOrientation(orientation)
+ }
+
+ override fun platform_setRequestedOrientation(orientation: Int) {
+ super.setRequestedOrientation(orientation)
+ }
+
+ override fun platform_setResult(resultCode: Int) {
+ super.setResult(resultCode)
+ }
+
+ override fun platform_setResult(resultCode: Int, data: Intent?) {
+ super.setResult(resultCode, data)
+ }
+
+ @Deprecated
+ override fun platform_setSecondaryProgress(secondaryProgress: Int) {
+ super.setSecondaryProgress(secondaryProgress)
+ }
+
+ override fun setShouldDockBigOverlays(shouldDock: Boolean) {
+ activityProxyWrapper?.public_setShouldDockBigOverlays(shouldDock) ?: super.setShouldDockBigOverlays(shouldDock)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun platform_setShouldDockBigOverlays(shouldDock: Boolean) {
+ super.setShouldDockBigOverlays(shouldDock)
+ }
+
+ override fun setShowWhenLocked(showWhenLocked: Boolean) {
+ activityProxyWrapper?.public_setShowWhenLocked(showWhenLocked) ?: super.setShowWhenLocked(showWhenLocked)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O_MR1)
+ override fun platform_setShowWhenLocked(showWhenLocked: Boolean) {
+ super.setShowWhenLocked(showWhenLocked)
+ }
+
+ override fun setTaskDescription(description: ActivityManager.TaskDescription?) {
+ activityProxyWrapper?.public_setTaskDescription(description) ?: super.setTaskDescription(description)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_setTaskDescription(description: ActivityManager.TaskDescription?) {
+ super.setTaskDescription(description)
+ }
+
+ override fun setTitle(titleId: Int) {
+ activityProxyWrapper?.public_setTitle(titleId) ?: super.setTitle(titleId)
+ }
+
+ override fun platform_setTitle(titleId: Int) {
+ super.setTitle(titleId)
+ }
+
+ override fun setTitle(title: CharSequence?) {
+ activityProxyWrapper?.public_setTitle(title) ?: super.setTitle(title)
+ }
+
+ override fun platform_setTitle(title: CharSequence?) {
+ super.setTitle(title)
+ }
+
+ @Deprecated
+ override fun setTitleColor(textColor: Int) {
+ activityProxyWrapper?.public_setTitleColor(textColor) ?: super.setTitleColor(textColor)
+ }
+
+ @Deprecated
+ override fun platform_setTitleColor(textColor: Int) {
+ super.setTitleColor(textColor)
+ }
+
+ override fun setTranslucent(translucent: Boolean): Boolean {
+ return activityProxyWrapper?.public_setTranslucent(translucent) ?: super.setTranslucent(translucent)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ override fun platform_setTranslucent(translucent: Boolean): Boolean {
+ return super.setTranslucent(translucent)
+ }
+
+ override fun setTurnScreenOn(turnScreenOn: Boolean) {
+ activityProxyWrapper?.public_setTurnScreenOn(turnScreenOn) ?: super.setTurnScreenOn(turnScreenOn)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O_MR1)
+ override fun platform_setTurnScreenOn(turnScreenOn: Boolean) {
+ super.setTurnScreenOn(turnScreenOn)
+ }
+ override fun setVisible(visible: Boolean) {
+ activityProxyWrapper?.public_setVisible(visible) ?: super.setVisible(visible)
+ }
+
+ override fun platform_setVisible(visible: Boolean) {
+ super.setVisible(visible)
+ }
+
+ override fun platform_setVolumeControlStream(streamType: Int) {
+ super.setVolumeControlStream(streamType)
+ }
+
+ override fun setVrModeEnabled(enabled: Boolean, requestedComponent: ComponentName) {
+ activityProxyWrapper?.public_setVrModeEnabled(enabled, requestedComponent) ?: super.setVrModeEnabled(enabled, requestedComponent)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_setVrModeEnabled(enabled: Boolean, requestedComponent: ComponentName) {
+ super.setVrModeEnabled(enabled, requestedComponent)
+ }
+
+ override fun shouldDockBigOverlays(): Boolean {
+ return activityProxyWrapper?.public_shouldDockBigOverlays() ?: super.shouldDockBigOverlays()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun platform_shouldDockBigOverlays(): Boolean {
+ return super.shouldDockBigOverlays()
+ }
+
+ override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
+ return activityProxyWrapper?.public_shouldShowRequestPermissionRationale(permission) ?: super.shouldShowRequestPermissionRationale(permission)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_shouldShowRequestPermissionRationale(permission: String): Boolean {
+ return super.shouldShowRequestPermissionRationale(permission)
+ }
+
+ override fun shouldShowRequestPermissionRationale(permission: String, userId: Int): Boolean {
+ return activityProxyWrapper?.public_shouldShowRequestPermissionRationale(permission, userId) ?: super.shouldShowRequestPermissionRationale(permission, userId)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun platform_shouldShowRequestPermissionRationale(permission: String, userId: Int): Boolean {
+ return super.shouldShowRequestPermissionRationale(permission, userId)
+ }
+
+ override fun shouldUpRecreateTask(targetIntent: Intent?): Boolean {
+ return activityProxyWrapper?.public_shouldUpRecreateTask(targetIntent) ?: super.shouldUpRecreateTask(targetIntent)
+ }
+
+ override fun platform_shouldUpRecreateTask(targetIntent: Intent?): Boolean {
+ return super.shouldUpRecreateTask(targetIntent)
+ }
+
+ override fun showAssist(args: Bundle?): Boolean {
+ return activityProxyWrapper?.public_showAssist(args) ?: super.showAssist(args)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_showAssist(args: Bundle?): Boolean {
+ return super.showAssist(args)
+ }
+
+ override fun showLockTaskEscapeMessage() {
+ activityProxyWrapper?.public_showLockTaskEscapeMessage() ?: super.showLockTaskEscapeMessage()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_showLockTaskEscapeMessage() {
+ super.showLockTaskEscapeMessage()
+ }
+
+ override fun startActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return activityProxyWrapper?.public_startActionMode(callback) ?: super.startActionMode(callback)
+ }
+
+ override fun platform_startActionMode(callback: ActionMode.Callback?): ActionMode? {
+ return super.startActionMode(callback)
+ }
+
+ override fun startActionMode(callback: ActionMode.Callback?, type: Int): ActionMode? {
+ return activityProxyWrapper?.public_startActionMode(callback, type) ?: super.startActionMode(callback, type)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun platform_startActionMode(callback: ActionMode.Callback?, type: Int): ActionMode? {
+ return super.startActionMode(callback, type)
+ }
+
+ override fun startActivities(intents: Array) {
+ activityProxyWrapper?.public_startActivities(intents) ?: super.startActivities(intents)
+ }
+
+ override fun platform_startActivities(intents: Array) {
+ super.startActivities(intents)
+ }
+
+ override fun startActivities(intents: Array, options: Bundle?) {
+ activityProxyWrapper?.public_startActivities(intents, options) ?: super.startActivities(intents, options)
+ }
+
+ override fun platform_startActivities(intents: Array, options: Bundle?) {
+ super.startActivities(intents, options)
+ }
+
+ override fun startActivity(intent: Intent?) {
+ activityProxyWrapper?.public_startActivity(intent) ?: super.startActivity(intent)
+ }
+
+ override fun platform_startActivity(intent: Intent?) {
+ super.startActivity(intent)
+ }
+
+ override fun startActivity(intent: Intent?, options: Bundle?) {
+ activityProxyWrapper?.public_startActivity(intent, options) ?: super.startActivity(intent, options)
+ }
+
+ override fun platform_startActivity(intent: Intent?, options: Bundle?) {
+ super.startActivity(intent, options)
+ }
+
+ override fun startActivityForResult(intent: Intent?, requestCode: Int) {
+ activityProxyWrapper?.public_startActivityForResult(intent, requestCode) ?: super.startActivityForResult(intent, requestCode)
+ }
+
+ override fun platform_startActivityForResult(intent: Intent?, requestCode: Int) {
+ super.startActivityForResult(intent, requestCode)
+ }
+
+ override fun startActivityForResult(intent: Intent?, requestCode: Int, options: Bundle?) {
+ activityProxyWrapper?.public_startActivityForResult(intent, requestCode, options) ?: super.startActivityForResult(intent, requestCode, options)
+ }
+
+ override fun platform_startActivityForResult(intent: Intent?, requestCode: Int, options: Bundle?) {
+ super.startActivityForResult(intent, requestCode, options)
+ }
+
+ fun startActivityForResultAsUser(intent: Intent?, requestCode: Int, options: Bundle?, user: UserHandle?) {
+ throw UnsupportedOperationException("not supported @SystemApi")
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun platform_startActivityForResultAsUser(intent: Intent?, requestCode: Int, options: Bundle?, user: UserHandle?) {
+ throw UnsupportedOperationException("not supported @SystemApi")
+ }
+
+ fun startActivityForResultAsUser(intent: Intent?, requestCode: Int, user: UserHandle?) {
+ throw UnsupportedOperationException("not supported @SystemApi")
+ }
+
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ override fun platform_startActivityForResultAsUser(intent: Intent?, requestCode: Int, user: UserHandle?) {
+ throw UnsupportedOperationException("not supported @SystemApi")
+ }
+
+ fun startActivityForResultAsUser(intent: Intent?, permission: String?, requestCode: Int, options: Bundle?, user: UserHandle?) {
+ throw UnsupportedOperationException("not supported @SystemApi")
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun platform_startActivityForResultAsUser(intent: Intent?, permission: String?, requestCode: Int, options: Bundle?, user: UserHandle?) {
+ throw UnsupportedOperationException("not supported @SystemApi")
+ }
+
+ @Deprecated
+ override fun startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int) {
+ activityProxyWrapper?.public_startActivityFromChild(child, intent, requestCode) ?: super.startActivityFromChild(child, intent, requestCode)
+ }
+
+ override fun platform_startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int) {
+ super.startActivityFromChild(child, intent, requestCode)
+ }
+
+ @Deprecated
+ override fun startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int, options: Bundle?) {
+ activityProxyWrapper?.public_startActivityFromChild(child, intent, requestCode, options) ?: super.startActivityFromChild(child, intent, requestCode, options)
+ }
+
+ @Deprecated
+ override fun platform_startActivityFromChild(child: Activity, intent: Intent?, requestCode: Int, options: Bundle?) {
+ super.startActivityFromChild(child, intent, requestCode, options)
+ }
+
+ @Deprecated
+ override fun startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int) {
+ activityProxyWrapper?.public_startActivityFromFragment(fragment, intent, requestCode) ?: super.startActivityFromFragment(fragment, intent, requestCode)
+ }
+
+ @Deprecated
+ override fun platform_startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int) {
+ super.startActivityFromFragment(fragment, intent, requestCode)
+ }
+
+ @Deprecated
+ override fun startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int, options: Bundle?) {
+ activityProxyWrapper?.public_startActivityFromFragment(fragment, intent, requestCode, options) ?: super.startActivityFromFragment(fragment, intent, requestCode, options)
+ }
+
+ @Deprecated
+ override fun platform_startActivityFromFragment(fragment: Fragment, intent: Intent?, requestCode: Int, options: Bundle?) {
+ super.startActivityFromFragment(fragment, intent, requestCode, options)
+ }
+
+ override fun startActivityIfNeeded(intent: Intent, requestCode: Int): Boolean {
+ return activityProxyWrapper?.public_startActivityIfNeeded(intent, requestCode) ?: super.startActivityIfNeeded(intent, requestCode)
+ }
+
+ override fun platform_startActivityIfNeeded(intent: Intent, requestCode: Int): Boolean {
+ return super.startActivityIfNeeded(intent, requestCode)
+ }
+
+ override fun startActivityIfNeeded(intent: Intent, requestCode: Int, options: Bundle?): Boolean {
+ return activityProxyWrapper?.public_startActivityIfNeeded(intent, requestCode, options) ?: super.startActivityIfNeeded(intent, requestCode, options)
+ }
+
+ override fun platform_startActivityIfNeeded(intent: Intent, requestCode: Int, options: Bundle?): Boolean {
+ return super.startActivityIfNeeded(intent, requestCode, options)
+ }
+
+ override fun startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ activityProxyWrapper?.public_startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags) ?: super.startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ override fun platform_startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ super.startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ override fun startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ activityProxyWrapper?.public_startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags, options) ?: super.startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ override fun platform_startIntentSender(intentSender: IntentSender, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ super.startIntentSender(intentSender, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ override fun startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ activityProxyWrapper?.public_startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags) ?: super.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ override fun platform_startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ super.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ override fun startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ activityProxyWrapper?.public_startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options) ?: super.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ override fun platform_startIntentSenderForResult(intentSender: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ super.startIntentSenderForResult(intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ @Deprecated
+ override fun startIntentSenderFromChild(child: Activity?, intentSender: IntentSender?, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ activityProxyWrapper?.public_startIntentSenderFromChild(child, intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags) ?: super.startIntentSenderFromChild(child, intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ @Deprecated
+ override fun platform_startIntentSenderFromChild(child: Activity?, intentSender: IntentSender?, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int) {
+ super.startIntentSenderFromChild(child, intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags)
+ }
+
+ @Deprecated
+ override fun startIntentSenderFromChild(child: Activity?, intentSender: IntentSender?, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ activityProxyWrapper?.public_startIntentSenderFromChild(child, intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options) ?: super.startIntentSenderFromChild(child, intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ @Deprecated
+ override fun platform_startIntentSenderFromChild(child: Activity?, intentSender: IntentSender?, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle?) {
+ super.startIntentSenderFromChild(child, intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options)
+ }
+
+ override fun startLocalVoiceInteraction(privateOptions: Bundle?) {
+ activityProxyWrapper?.public_startLocalVoiceInteraction(privateOptions) ?: super.startLocalVoiceInteraction(privateOptions)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_startLocalVoiceInteraction(privateOptions: Bundle?) {
+ super.startLocalVoiceInteraction(privateOptions)
+ }
+
+ override fun startLockTask() {
+ activityProxyWrapper?.public_startLockTask() ?: super.startLockTask()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_startLockTask() {
+ super.startLockTask()
+ }
+
+ @Deprecated
+ override fun startManagingCursor(cursor: Cursor?) {
+ activityProxyWrapper?.public_startManagingCursor(cursor) ?: super.startManagingCursor(cursor)
+ }
+
+ @Deprecated
+ override fun platform_startManagingCursor(cursor: Cursor?) {
+ super.startManagingCursor(cursor)
+ }
+
+ override fun startNextMatchingActivity(intent: Intent): Boolean {
+ return activityProxyWrapper?.public_startNextMatchingActivity(intent) ?: super.startNextMatchingActivity(intent)
+ }
+
+ override fun platform_startNextMatchingActivity(intent: Intent): Boolean {
+ return super.startNextMatchingActivity(intent)
+ }
+
+ override fun startNextMatchingActivity(intent: Intent, options: Bundle?): Boolean {
+ return activityProxyWrapper?.public_startNextMatchingActivity(intent, options) ?: super.startNextMatchingActivity(intent, options)
+ }
+
+ override fun platform_startNextMatchingActivity(intent: Intent, options: Bundle?): Boolean {
+ return super.startNextMatchingActivity(intent, options)
+ }
+
+ override fun startPostponedEnterTransition() {
+ activityProxyWrapper?.public_startPostponedEnterTransition() ?: super.startPostponedEnterTransition()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_startPostponedEnterTransition() {
+ super.startPostponedEnterTransition()
+ }
+
+ override fun startSearch(initialQuery: String?, selectInitialQuery: Boolean, appSearchData: Bundle?, globalSearch: Boolean) {
+ activityProxyWrapper?.public_startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch) ?: super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch)
+ }
+
+ override fun platform_startSearch(initialQuery: String?, selectInitialQuery: Boolean, appSearchData: Bundle?, globalSearch: Boolean) {
+ super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch)
+ }
+
+ override fun stopLocalVoiceInteraction() {
+ activityProxyWrapper?.public_stopLocalVoiceInteraction() ?: super.stopLocalVoiceInteraction()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun platform_stopLocalVoiceInteraction() {
+ super.stopLocalVoiceInteraction()
+ }
+
+ override fun stopLockTask() {
+ activityProxyWrapper?.public_stopLockTask() ?: super.stopLockTask()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun platform_stopLockTask() {
+ super.stopLockTask()
+ }
+
+ @Deprecated
+ override fun stopManagingCursor(cursor: Cursor?) {
+ activityProxyWrapper?.public_stopManagingCursor(cursor) ?: super.stopManagingCursor(cursor)
+ }
+
+ @Deprecated
+ override fun platform_stopManagingCursor(cursor: Cursor?) {
+ super.stopManagingCursor(cursor)
+ }
+
+ override fun takeKeyEvents(get: Boolean) {
+ activityProxyWrapper?.public_takeKeyEvents(get) ?: super.takeKeyEvents(get)
+ }
+
+ override fun platform_takeKeyEvents(get: Boolean) {
+ super.takeKeyEvents(get)
+ }
+
+ override fun triggerSearch(query: String?, appSearchData: Bundle?) {
+ activityProxyWrapper?.public_triggerSearch(query, appSearchData) ?: super.triggerSearch(query, appSearchData)
+ }
+
+ override fun platform_triggerSearch(query: String?, appSearchData: Bundle?) {
+ super.triggerSearch(query, appSearchData)
+ }
+
+ override fun unregisterActivityLifecycleCallbacks(callback: Application.ActivityLifecycleCallbacks) {
+ activityProxyWrapper?.public_unregisterActivityLifecycleCallbacks(callback) ?: super.unregisterActivityLifecycleCallbacks(callback)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ override fun platform_unregisterActivityLifecycleCallbacks(callback: Application.ActivityLifecycleCallbacks) {
+ super.unregisterActivityLifecycleCallbacks(callback)
+ }
+
+ override fun unregisterForContextMenu(view: View?) {
+ activityProxyWrapper?.public_unregisterForContextMenu(view) ?: super.unregisterForContextMenu(view)
+ }
+
+ override fun platform_unregisterForContextMenu(view: View?) {
+ super.unregisterForContextMenu(view)
+ }
+
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraComponentProxy.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraComponentProxy.kt
new file mode 100644
index 0000000000..3e1684b3e4
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraComponentProxy.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.content.Context
+
+class ChimeraComponentProxy {
+ companion object {
+
+ @JvmStatic
+ fun bindComponentProxy(provider: ChimeraModuleContextProvider, obj: Any, callback: ChimeraProxyCallback, context: Context) {
+ val moduleContext = provider.createModuleContext(obj, callback.javaClass, context)
+ callback.setProxyCallbacks(obj, moduleContext)
+ provider.setProxyWrapper(callback, moduleContext)
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFallbackActImpl.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFallbackActImpl.kt
new file mode 100644
index 0000000000..73f0785cc5
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFallbackActImpl.kt
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import com.google.android.chimera.android.Activity
+import com.google.android.chimera.config.ModuleDownloadRegistry
+
+private const val REQUEST_MODULE_DOWNLOAD = 0x4349
+private const val STATE_DOWNLOAD_FLOW_LAUNCHED = "download_flow_launched"
+
+open class ChimeraFallbackActImpl : Activity() {
+
+ private var requestedFeatureNames: String? = null
+ private var downloadFlowLaunched = false
+
+ override fun onCreate(bundle: Bundle?) {
+ super.onCreate(bundle)
+ downloadFlowLaunched = bundle?.getBoolean(STATE_DOWNLOAD_FLOW_LAUNCHED) ?: false
+ Log.d(TAG, "onCreate ${getContainerActivity().javaClass.name}")
+
+ val containerActivity = getContainerActivity()
+ requestedFeatureNames = ModuleDownloadRegistry.requestedFeatureNamesForActivity(
+ containerActivity,
+ containerActivity.javaClass.name
+ )
+ Log.d(TAG, "Requested features: $requestedFeatureNames")
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (downloadFlowLaunched) return
+ downloadFlowLaunched = true
+
+ // The shared page explains the module and its permissions, then enforces authorization before download.
+ Log.w(TAG, "Module not available locally, features: $requestedFeatureNames")
+ val containerActivity = getContainerActivity()
+ val downloadIntent = ModuleDownloadRegistry.createModuleDownloadIntent(
+ containerActivity,
+ requestedFeatureNames
+ )
+ if (downloadIntent == null) {
+ Log.w(TAG, "No module download configured for features: $requestedFeatureNames")
+ finish()
+ return
+ }
+ // Keep the original container Activity (and therefore its caller/result chain) alive while
+ // the user downloads and imports the module. ModuleDownloadActivity only returns RESULT_OK
+ // after it has verified that the requested feature is now installed.
+ startActivityForResult(downloadIntent, REQUEST_MODULE_DOWNLOAD)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putBoolean(STATE_DOWNLOAD_FLOW_LAUNCHED, downloadFlowLaunched)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode != REQUEST_MODULE_DOWNLOAD) {
+ super.onActivityResult(requestCode, resultCode, data)
+ return
+ }
+
+ if (resultCode == RESULT_OK) {
+ // Re-resolve the dynamic implementation in the same container Activity. Recreating it
+ // preserves the external caller and delivers the module's eventual result normally.
+ getContainerActivity().recreate()
+ } else {
+ finish()
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFileApk.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFileApk.kt
new file mode 100644
index 0000000000..ee494dc571
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFileApk.kt
@@ -0,0 +1,130 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import com.google.android.chimera.loader.BaseFileModuleApk
+import dalvik.system.DelegateLastClassLoader
+import dalvik.system.PathClassLoader
+import java.io.File
+import java.io.IOException
+import java.io.InvalidObjectException
+import java.security.MessageDigest
+import java.util.zip.ZipFile
+
+class ChimeraFileApk(
+ context: Context,
+ moduleType: Int,
+ private val archiveFilePath: String,
+ private val expectedSha256: String? = null
+): BaseFileModuleApk(context, 3, moduleType) {
+
+ var apkPath: String? = null
+ var className: String? = null
+
+ companion object {
+ private const val TAG = "ChimeraFileApk"
+
+ @JvmStatic
+ fun buildNativeLibPaths(apkPath: String): List {
+ val abis: List = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (android.os.Process.is64Bit()) {
+ Build.SUPPORTED_64_BIT_ABIS.toList()
+ } else {
+ Build.SUPPORTED_32_BIT_ABIS.toList()
+ }
+ } else {
+ listOfNotNull(Build.CPU_ABI, Build.CPU_ABI2.takeIf { it.isNotEmpty() })
+ }
+
+ val nativeDir = File(File(apkPath).parentFile, "n")
+
+ return abis.map { abi ->
+ if (nativeDir.exists()) "$nativeDir/$abi" else "$apkPath!/lib/$abi"
+ }
+ }
+ }
+
+ fun getFullApkPath(): String? {
+ val abis = buildNativeLibPaths(archiveFilePath)
+ return if (abis.isNotEmpty()) {
+ abis.joinToString(";")
+ } else {
+ null
+ }
+ }
+
+
+ private fun requireLoadableApk() {
+ val file = File(archiveFilePath)
+ if (!file.isFile || !file.canRead()) {
+ throw InvalidObjectException("Module APK is not readable: $archiveFilePath")
+ }
+ if (!isValidApk(file)) {
+ throw InvalidObjectException("Module APK is invalid: $archiveFilePath")
+ }
+ if (!expectedSha256.isNullOrEmpty()) {
+ val actual = sha256Hex(file)
+ if (!actual.equals(expectedSha256, ignoreCase = true)) {
+ throw InvalidObjectException("Module APK digest mismatch: $archiveFilePath")
+ }
+ }
+ }
+
+ private fun isValidApk(file: File): Boolean {
+ return try {
+ ZipFile(file).use { zip -> zip.getEntry("AndroidManifest.xml") != null }
+ } catch (_: Exception) {
+ false
+ }
+ }
+
+ private fun sha256Hex(file: File): String {
+ val md = MessageDigest.getInstance("SHA-256")
+ file.inputStream().buffered().use { input ->
+ val buf = ByteArray(8192)
+ while (true) {
+ val read = input.read(buf)
+ if (read < 0) break
+ md.update(buf, 0, read)
+ }
+ }
+ return md.digest().joinToString("") { "%02x".format(it) }
+ }
+
+ override fun createClassLoader(parentClassLoader: ClassLoader): ClassLoader {
+ requireLoadableApk()
+ val args = this.apkPath ?: getFullApkPath()
+ var canonicalPath = ""
+ try {
+ canonicalPath = File(archiveFilePath).canonicalPath
+ } catch (e: IOException) {
+ Log.w(TAG, "Unable to determine canonical path for apk \'$canonicalPath\'")
+ }
+
+ val newClassLoader = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ DelegateLastClassLoader(canonicalPath, args, parentClassLoader, false)
+ } else {
+ PathClassLoader(canonicalPath, args, parentClassLoader)
+ }
+
+ if (className != null) {
+ try {
+ newClassLoader.loadClass(className)
+ return newClassLoader
+ } catch (e: ClassNotFoundException) {
+ Log.w(TAG, "Failed to validate PathClassLoader for $archiveFilePath :$e")
+ throw InvalidObjectException("Can\'t load code for ${File(archiveFilePath).name}")
+ }
+ }
+ return newClassLoader
+ }
+
+ override fun getArchiveFilePath(): String {
+ return archiveFilePath
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraModuleContextProvider.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraModuleContextProvider.kt
new file mode 100644
index 0000000000..b15b18a76d
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraModuleContextProvider.kt
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.content.Context
+
+interface ChimeraModuleContextProvider {
+ fun createContextWrapper(proxy: Any?, baseContext: Context): Context
+ fun createModuleContext(module: Any?, moduleClass: Class<*>?, baseContext: Context): Context
+ fun setProxyWrapper(proxy: Any?, context: Context)
+ fun setProxyWrapper(moduleName: String?, proxy: Any?, context: Context)
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraPermissionActImpl.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraPermissionActImpl.kt
new file mode 100644
index 0000000000..38d71f63b5
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraPermissionActImpl.kt
@@ -0,0 +1,70 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import com.google.android.chimera.android.Activity
+import com.google.android.chimera.config.ModuleDownloadRegistry
+
+private val PERMISSION_TAG: String = ChimeraPermissionActImpl::class.java.simpleName
+private const val REQUEST_MODULE_PERMISSION = 0x4348
+private const val STATE_PERMISSION_FLOW_LAUNCHED = "permission_flow_launched"
+
+/** Handles an installed Chimera Activity whose runtime permission was revoked after installation. */
+open class ChimeraPermissionActImpl : Activity() {
+ private var requestedFeatureNames: String? = null
+ private var permissionFlowLaunched = false
+
+ override fun onCreate(bundle: Bundle?) {
+ super.onCreate(bundle)
+ permissionFlowLaunched = bundle?.getBoolean(STATE_PERMISSION_FLOW_LAUNCHED) ?: false
+ val containerActivity = getContainerActivity()
+ requestedFeatureNames = ModuleDownloadRegistry.requestedFeatureNamesForActivity(
+ containerActivity,
+ containerActivity.javaClass.name
+ )
+ Log.d(PERMISSION_TAG, "Requested features: $requestedFeatureNames")
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (permissionFlowLaunched) return
+ permissionFlowLaunched = true
+
+ val containerActivity = getContainerActivity()
+ val permissionIntent = ModuleDownloadRegistry.createModulePermissionIntent(
+ containerActivity,
+ requestedFeatureNames
+ )
+ if (permissionIntent == null) {
+ Log.w(PERMISSION_TAG, "No permission flow configured for features: $requestedFeatureNames")
+ finish()
+ return
+ }
+ startActivityForResult(permissionIntent, REQUEST_MODULE_PERMISSION)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putBoolean(STATE_PERMISSION_FLOW_LAUNCHED, permissionFlowLaunched)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode != REQUEST_MODULE_PERMISSION) {
+ super.onActivityResult(requestCode, resultCode, data)
+ return
+ }
+
+ if (resultCode == RESULT_OK) {
+ // Recreate the same container Activity so its original caller and result chain remain intact.
+ getContainerActivity().recreate()
+ } else {
+ finish()
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraProxyCallback.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraProxyCallback.kt
new file mode 100644
index 0000000000..c769c449a3
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraProxyCallback.kt
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.content.Context
+
+interface ChimeraProxyCallback {
+ fun setProxyCallbacks(arg1: Any?, arg2: Context?)
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ContainerApk.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ContainerApk.kt
new file mode 100644
index 0000000000..d3a21f18e6
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ContainerApk.kt
@@ -0,0 +1,66 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import com.google.android.chimera.context.ModuleContext
+import com.google.android.chimera.loader.ChimeraModuleApk
+import dalvik.system.DelegateLastClassLoader
+import dalvik.system.PathClassLoader
+
+class ContainerApk(context: Context) : ChimeraModuleApk(
+ ModuleContext.getModuleContext(context)?.baseContext ?: context,
+ 1,
+ 0
+) {
+ private val packageName: String = appContext.packageName
+
+ override fun getApplicationInfo(): ApplicationInfo {
+ return appContext.createPackageContext(packageName, 0).applicationInfo
+ }
+
+ override fun createClassLoader(parentClassLoader: ClassLoader): ClassLoader {
+ val appInfo = appContext.packageManager.getApplicationInfo(packageName, 0)
+ val nativeLibPaths = mutableListOf()
+ if (appInfo.nativeLibraryDir != null) {
+ nativeLibPaths.add(appInfo.nativeLibraryDir)
+ }
+
+ if (appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0) {
+ val abis = if (Build.VERSION.SDK_INT >= 23) {
+ if (android.os.Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS.toList()
+ else Build.SUPPORTED_32_BIT_ABIS.toList()
+ } else {
+ listOfNotNull(Build.CPU_ABI, Build.CPU_ABI2.takeIf { it.isNotEmpty() })
+ }
+ abis.forEach { abi -> nativeLibPaths.add("${appInfo.sourceDir}!/lib/$abi") }
+ }
+
+ val nativePathString = if (nativeLibPaths.isEmpty()) null else nativeLibPaths.joinToString(";")
+ val apkPath = appInfo.sourceDir
+ val args = nativePathString
+
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ DelegateLastClassLoader(apkPath, args, parentClassLoader, false)
+ } else {
+ PathClassLoader(apkPath, args, parentClassLoader)
+ }
+ }
+
+ override fun getArchiveFilePath(): String? {
+ return try {
+ appContext.packageManager.getApplicationInfo(packageName, 0).sourceDir
+ } catch (_: PackageManager.NameNotFoundException) {
+ null
+ }
+ }
+
+ override fun toString(): String {
+ return "ContainerApk($packageName)"
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleDownloadActivity.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleDownloadActivity.kt
new file mode 100644
index 0000000000..6b8928e287
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleDownloadActivity.kt
@@ -0,0 +1,572 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.app.PendingIntent
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.util.Log
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.config.ModuleDownloadRegistry
+import com.google.android.gms.common.Feature
+import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer
+import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest
+import org.microg.gms.chimera.core.R
+import java.util.UUID
+
+const val TAG = "ChimeraModuleDownload"
+private const val REQUEST_MODULE_PERMISSIONS = 1
+private const val STATE_PERMISSION_REQUESTED = "permission_requested"
+private const val STATE_SETTINGS_REQUIRED = "settings_required"
+private const val STATE_WAITING_FOR_SETTINGS = "waiting_for_settings"
+private const val STATE_WAITING_FOR_IMPORT = "waiting_for_import"
+private const val STATE_WAITING_FOR_DOWNLOAD_SELECTION = "waiting_for_download_selection"
+private const val STATE_AFTER_AUTHORIZATION_ACTION = "after_authorization_action"
+private const val STATE_STATUS_MESSAGE = "status_message"
+private const val STATE_REQUEST_ID = "request_id"
+private const val STATE_CURRENT_MODULE_INDEX = "current_module_index"
+
+/**
+ * Shows a module action and its permission requirements in one page. The requested action is only
+ * performed after every permission registered for the module has been granted.
+ */
+class ModuleDownloadActivity : Activity() {
+ private lateinit var requestId: String
+ private lateinit var requestedFeatures: List
+ private lateinit var modules: List
+ private lateinit var module: ModuleDownloadRegistry.DownloadableModule
+ private var currentModuleIndex = 0
+ private var apiFeatureRequest: ApiFeatureRequest? = null
+ private var containerComponentClassName: String? = null
+ private var afterAuthorizationAction = ACTION_DOWNLOAD_MODULE
+ private var dialog: AlertDialog? = null
+ private var permissionRequested = false
+ private var settingsRequired = false
+ private var waitingForSettings = false
+ private var waitingForImport = false
+ private var waitingForDownloadSelection = false
+ private var actionStarted = false
+ private var statusMessageRes: Int? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ requestId = savedInstanceState?.getString(STATE_REQUEST_ID)
+ ?: intent.getStringExtra(EXTRA_REQUEST_ID)
+ ?: UUID.randomUUID().toString()
+ if (!DynamicModuleSettings.isAvailable(this)) {
+ cancelAndFinish()
+ return
+ }
+
+ afterAuthorizationAction = savedInstanceState?.getInt(STATE_AFTER_AUTHORIZATION_ACTION)
+ ?: intent.getIntExtra(
+ EXTRA_AFTER_AUTHORIZATION_ACTION,
+ ACTION_DOWNLOAD_MODULE
+ )
+
+ val intentFeatures = ModuleDownloadRegistry.requestedFeaturesFromIntent(intent)
+ val persistedRequest = if (afterAuthorizationAction == ACTION_DOWNLOAD_MODULE) {
+ PendingModuleRequestStore.load(this, requestId)
+ } else {
+ // A permission-only launch must never inherit a download hand-off for this request.
+ PendingModuleRequestStore.remove(this, requestId)
+ null
+ }
+ requestedFeatures = persistedRequest?.features ?: intentFeatures
+ apiFeatureRequest = intent
+ .getByteArrayExtra(ModuleDownloadRegistry.EXTRA_API_FEATURE_REQUEST)
+ ?.let { bytes ->
+ runCatching {
+ SafeParcelableSerializer.deserializeFromBytes(
+ bytes,
+ ApiFeatureRequest.CREATOR,
+ )
+ }.onFailure { error ->
+ Log.w(TAG, "Unable to deserialize API feature request", error)
+ }.getOrNull()
+ }
+ ?: ApiFeatureRequest().apply {
+ features = requestedFeatures.map { Feature(it.name, it.minVersion) }
+ }
+ Log.d(TAG, "onCreate: apiFeatureRequest features=${apiFeatureRequest?.features?.size}")
+ modules = ModuleDownloadRegistry.resolveModules(requestedFeatures)
+ if (modules.isEmpty()) {
+ Log.w(TAG, "No downloadable module for features: $requestedFeatures")
+ PendingModuleRequestStore.remove(this, requestId)
+ finish()
+ return
+ }
+ containerComponentClassName = intent.getStringExtra(EXTRA_CONTAINER_COMPONENT_CLASS_NAME)
+ ?: persistedRequest?.componentClassName
+ val restoredModuleIndex = when {
+ savedInstanceState?.containsKey(STATE_CURRENT_MODULE_INDEX) == true ->
+ savedInstanceState.getInt(STATE_CURRENT_MODULE_INDEX).coerceIn(modules.indices)
+
+ persistedRequest != null -> modules
+ .indexOfFirst { it.catalogId == persistedRequest.currentCatalogId }
+ .takeIf { it >= 0 }
+
+ else -> null
+ }
+ currentModuleIndex = restoredModuleIndex
+ ?: modules.indices.firstOrNull { !isModuleInstalled(modules[it]) }
+ ?: 0
+ module = modules[currentModuleIndex]
+ if (savedInstanceState == null && persistedRequest != null) {
+ afterAuthorizationAction = persistedRequest.afterAuthorizationAction
+ }
+
+ permissionRequested = savedInstanceState?.getBoolean(STATE_PERMISSION_REQUESTED) ?: false
+ settingsRequired = savedInstanceState?.getBoolean(STATE_SETTINGS_REQUIRED) ?: false
+ waitingForSettings = savedInstanceState?.getBoolean(STATE_WAITING_FOR_SETTINGS) ?: false
+ waitingForImport = savedInstanceState?.getBoolean(STATE_WAITING_FOR_IMPORT)
+ ?: (persistedRequest != null)
+ waitingForDownloadSelection = savedInstanceState
+ ?.getBoolean(STATE_WAITING_FOR_DOWNLOAD_SELECTION)
+ ?: (persistedRequest != null && !persistedRequest.downloadTargetSelected)
+ statusMessageRes = savedInstanceState
+ ?.getInt(STATE_STATUS_MESSAGE)
+ ?.takeIf { it != 0 }
+ showModulePage()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (!::module.isInitialized) return
+ if (!DynamicModuleSettings.isAvailable(this)) {
+ cancelAndFinish()
+ return
+ }
+
+ if (waitingForDownloadSelection) {
+ waitingForDownloadSelection = false
+ val targetSelected = PendingModuleRequestStore
+ .load(this, requestId)
+ ?.downloadTargetSelected == true
+ if (!targetSelected) {
+ waitingForImport = false
+ actionStarted = false
+ statusMessageRes = null
+ refreshModulePage()
+ }
+ }
+
+ if (waitingForImport) {
+ if (handleImportedModule()) return
+ actionStarted = false
+ refreshModulePage()
+ } else if (afterAuthorizationAction == ACTION_DOWNLOAD_MODULE && isModuleInstalled(module)) {
+ handleImportedModule()
+ return
+ }
+
+ if (!waitingForSettings || actionStarted) return
+
+ waitingForSettings = false
+ if (missingPermissions().isEmpty()) {
+ performAuthorizedAction()
+ } else {
+ statusMessageRes = permissionDeniedMessage()
+ refreshModulePage()
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putBoolean(STATE_PERMISSION_REQUESTED, permissionRequested)
+ outState.putBoolean(STATE_SETTINGS_REQUIRED, settingsRequired)
+ outState.putBoolean(STATE_WAITING_FOR_SETTINGS, waitingForSettings)
+ outState.putBoolean(STATE_WAITING_FOR_IMPORT, waitingForImport)
+ outState.putBoolean(STATE_WAITING_FOR_DOWNLOAD_SELECTION, waitingForDownloadSelection)
+ outState.putInt(STATE_AFTER_AUTHORIZATION_ACTION, afterAuthorizationAction)
+ outState.putString(STATE_REQUEST_ID, requestId)
+ outState.putInt(STATE_CURRENT_MODULE_INDEX, currentModuleIndex)
+ statusMessageRes?.let { outState.putInt(STATE_STATUS_MESSAGE, it) }
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode != REQUEST_MODULE_PERMISSIONS) return
+
+ val missing = missingPermissions()
+ if (missing.isEmpty()) {
+ performAuthorizedAction()
+ return
+ }
+
+ settingsRequired = permissionRequested && missing.any {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+ !shouldShowRequestPermissionRationale(it.permission)
+ }
+ statusMessageRes = permissionDeniedMessage()
+ refreshModulePage()
+ }
+
+ private fun showModulePage() {
+ val moduleDialog = AlertDialog.Builder(this)
+ .setTitle(getString(R.string.chimera_module_download_page_title, getString(module.displayNameRes)))
+ .setMessage(buildPageMessage())
+ .setPositiveButton(primaryButtonText(), null)
+ .setNegativeButton(R.string.chimera_module_cancel) { _, _ -> cancelAndFinish() }
+ .setOnCancelListener { cancelAndFinish() }
+ .create()
+ moduleDialog.setOnShowListener {
+ moduleDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
+ handlePrimaryAction()
+ }
+ }
+ dialog = moduleDialog
+ moduleDialog.show()
+ }
+
+ private fun refreshModulePage() {
+ val moduleDialog = dialog ?: return
+ moduleDialog.setTitle(
+ getString(R.string.chimera_module_download_page_title, getString(module.displayNameRes))
+ )
+ moduleDialog.setMessage(buildPageMessage())
+ moduleDialog.getButton(AlertDialog.BUTTON_POSITIVE).apply {
+ setText(primaryButtonText())
+ isEnabled = !actionStarted
+ }
+ }
+
+ private fun buildPageMessage(): String {
+ val permissionItems = if (module.requiredPermissions.isEmpty()) {
+ getString(R.string.chimera_module_no_permissions)
+ } else {
+ module.requiredPermissions.joinToString("\n") { requirement ->
+ val state = if (isPermissionGranted(requirement.permission)) {
+ getString(R.string.chimera_module_permission_granted)
+ } else {
+ getString(R.string.chimera_module_permission_required)
+ }
+ getString(
+ R.string.chimera_module_permission_item,
+ getString(requirement.labelRes),
+ getString(requirement.descriptionRes),
+ state
+ )
+ }
+ }
+ return buildString {
+ if (afterAuthorizationAction == ACTION_CONTINUE_MODULE) {
+ append(getString(R.string.chimera_module_use_section_title))
+ append('\n')
+ append(getString(R.string.chimera_module_use_description, getString(module.displayNameRes)))
+ } else {
+ append(getString(R.string.chimera_module_download_section_title))
+ append('\n')
+ append(getString(R.string.chimera_module_download_description, getString(module.displayNameRes)))
+ }
+ append("\n\n")
+ append(getString(R.string.chimera_module_permission_section_title))
+ append('\n')
+ append(permissionItems)
+ append("\n\n")
+ append(
+ getString(
+ if (afterAuthorizationAction == ACTION_CONTINUE_MODULE) {
+ R.string.chimera_module_permission_continue_instruction
+ } else {
+ R.string.chimera_module_permission_instruction
+ }
+ )
+ )
+ statusMessageRes?.let {
+ append("\n\n")
+ append(getString(it))
+ }
+ if (waitingForImport) {
+ append("\n\n")
+ append(getString(R.string.chimera_module_waiting_for_import))
+ }
+ }
+ }
+
+ private fun primaryButtonText(): String {
+ val missingPermissions = missingPermissions()
+ return when {
+ missingPermissions.isEmpty() && waitingForImport ->
+ getString(R.string.chimera_module_download_again)
+
+ missingPermissions.isEmpty() -> getString(
+ if (afterAuthorizationAction == ACTION_CONTINUE_MODULE) {
+ R.string.chimera_module_continue
+ } else {
+ R.string.chimera_module_download
+ }
+ )
+
+ settingsRequired -> getString(R.string.chimera_module_open_permission_settings)
+ afterAuthorizationAction == ACTION_CONTINUE_MODULE ->
+ getString(R.string.chimera_module_authorize_and_continue)
+
+ else -> getString(R.string.chimera_module_authorize_and_download)
+ }
+ }
+
+ private fun handlePrimaryAction() {
+ if (!DynamicModuleSettings.isAvailable(this)) {
+ cancelAndFinish()
+ return
+ }
+ val missing = missingPermissions()
+ if (missing.isEmpty()) {
+ performAuthorizedAction()
+ return
+ }
+ if (settingsRequired) {
+ openPermissionSettings()
+ return
+ }
+
+ permissionRequested = true
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ requestPermissions(
+ missing.map { it.permission }.distinct().toTypedArray(),
+ REQUEST_MODULE_PERMISSIONS
+ )
+ }
+ }
+
+ private fun openPermissionSettings() {
+ waitingForSettings = true
+ try {
+ startActivity(
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", packageName, null)
+ }
+ )
+ } catch (e: ActivityNotFoundException) {
+ waitingForSettings = false
+ Log.w(TAG, "Unable to open application permission settings", e)
+ statusMessageRes = R.string.chimera_module_permission_settings_unavailable
+ refreshModulePage()
+ }
+ }
+
+ private fun performAuthorizedAction() {
+ if (!DynamicModuleSettings.isAvailable(this)) {
+ cancelAndFinish()
+ return
+ }
+ // Re-check immediately before leaving this page. This is the hard authorization gate.
+ if (missingPermissions().isNotEmpty()) {
+ statusMessageRes = permissionDeniedMessage()
+ refreshModulePage()
+ return
+ }
+ if (afterAuthorizationAction == ACTION_CONTINUE_MODULE && !isModuleInstalled(module)) {
+ // The module may have been removed while the permission page was in front. Never return
+ // success into a container that can no longer resolve its dynamic implementation.
+ afterAuthorizationAction = ACTION_DOWNLOAD_MODULE
+ statusMessageRes = null
+ refreshModulePage()
+ return
+ }
+ if (afterAuthorizationAction == ACTION_DOWNLOAD_MODULE) {
+ val checkedCatalogId = module.catalogId
+ if (handleImportedModule()) return
+ // A multi-module request advanced to a new module: let the user read its explanation
+ // and permission list before opening that module's external download.
+ if (module.catalogId != checkedCatalogId) return
+ }
+ // handleImportedModule may have advanced a multi-module request to its next module.
+ if (missingPermissions().isNotEmpty()) {
+ statusMessageRes = permissionDeniedMessage()
+ refreshModulePage()
+ return
+ }
+ actionStarted = true
+ refreshModulePage()
+ if (afterAuthorizationAction == ACTION_CONTINUE_MODULE) {
+ setResult(RESULT_OK)
+ finish()
+ } else {
+ startExternalModuleDownload()
+ }
+ }
+
+ private fun startExternalModuleDownload() {
+ try {
+ waitingForImport = true
+ waitingForDownloadSelection = true
+ statusMessageRes = null
+ PendingModuleRequestStore.save(
+ this,
+ PendingModuleRequestStore.PendingRequest(
+ requestId = requestId,
+ features = requestedFeatures,
+ currentCatalogId = module.catalogId,
+ componentClassName = containerComponentClassName,
+ afterAuthorizationAction = afterAuthorizationAction,
+ taskId = taskId,
+ )
+ )
+ startActivity(
+ ModuleDownloadRegistry.createExternalDownloadChooserIntent(
+ module.downloadUrl,
+ getString(R.string.chimera_module_choose_download_app),
+ apiFeatureRequest,
+ createDownloadSelectionCallback(),
+ )
+ )
+ // The browser/download application does not return an import result. Keep this page and
+ // its original task alive until the downloaded file is opened with microG and committed.
+ actionStarted = false
+ } catch (e: ActivityNotFoundException) {
+ actionStarted = false
+ waitingForImport = false
+ waitingForDownloadSelection = false
+ PendingModuleRequestStore.remove(this, requestId)
+ Log.w(TAG, "No external application can download this module", e)
+ statusMessageRes = R.string.chimera_module_download_app_unavailable
+ refreshModulePage()
+ }
+ }
+
+ private fun createDownloadSelectionCallback() = PendingIntent.getBroadcast(
+ this,
+ requestId.hashCode(),
+ Intent(this, ModuleImportCompletionReceiver::class.java)
+ .setAction(ACTION_DOWNLOAD_TARGET_SELECTED)
+ .putExtra(EXTRA_REQUEST_ID, requestId),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT or if (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ ) {
+ // The chooser fills the selected component into this callback Intent.
+ PendingIntent.FLAG_MUTABLE
+ } else {
+ 0
+ },
+ ).intentSender
+
+ /**
+ * Returns true only when this Activity completed successfully. A successful `.mods` import can
+ * contain an unrelated module, so the requested module is always re-checked from persisted
+ * Chimera configuration before the original request is resumed.
+ */
+ private fun handleImportedModule(): Boolean {
+ if (!DynamicModuleSettings.isAvailable(this)) {
+ cancelAndFinish()
+ return true
+ }
+ if (!isModuleInstalled(module)) {
+ Log.d(TAG, "Import completed without requested module ${module.catalogId}")
+ return false
+ }
+
+ waitingForImport = false
+ waitingForDownloadSelection = false
+ actionStarted = false
+ PendingModuleRequestStore.remove(this, requestId)
+
+ val nextModuleIndex = modules.indices.firstOrNull { !isModuleInstalled(modules[it]) }
+ if (nextModuleIndex != null) {
+ currentModuleIndex = nextModuleIndex
+ module = modules[currentModuleIndex]
+ permissionRequested = false
+ settingsRequired = false
+ statusMessageRes = null
+ refreshModulePage()
+ return false
+ }
+
+ val moduleWithMissingPermission = modules.indices.firstOrNull { index ->
+ missingPermissions(modules[index]).isNotEmpty()
+ }
+ if (moduleWithMissingPermission != null) {
+ // Permission may have been revoked while the importer was in front. The module
+ // is installed now, so switch this same page to authorize-and-continue instead of offering
+ // another download.
+ currentModuleIndex = moduleWithMissingPermission
+ module = modules[currentModuleIndex]
+ afterAuthorizationAction = ACTION_CONTINUE_MODULE
+ statusMessageRes = R.string.chimera_module_permission_denied_continue
+ refreshModulePage()
+ return false
+ }
+
+ Log.i(TAG, "Verified imported modules for request $requestId; resuming original request")
+ setResult(RESULT_OK)
+ finish()
+ return true
+ }
+
+ private fun cancelAndFinish() {
+ if (::requestId.isInitialized) PendingModuleRequestStore.remove(this, requestId)
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+
+ private fun permissionDeniedMessage(): Int {
+ return if (afterAuthorizationAction == ACTION_CONTINUE_MODULE) {
+ R.string.chimera_module_permission_denied_continue
+ } else {
+ R.string.chimera_module_permission_denied
+ }
+ }
+
+ private fun missingPermissions(): List {
+ return missingPermissions(module)
+ }
+
+ private fun missingPermissions(
+ targetModule: ModuleDownloadRegistry.DownloadableModule,
+ ): List {
+ return targetModule.requiredPermissions.filterNot { isPermissionGranted(it.permission) }
+ }
+
+ private fun isModuleInstalled(
+ targetModule: ModuleDownloadRegistry.DownloadableModule,
+ ): Boolean {
+ val componentForModule = containerComponentClassName?.takeIf {
+ modules.size == 1 || it in targetModule.requiredComponentClassNames
+ }
+ return ModuleDownloadRegistry.isModuleInstalled(
+ this,
+ targetModule,
+ requestedFeatures,
+ componentForModule,
+ )
+ }
+
+ private fun isPermissionGranted(permission: String): Boolean {
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
+ checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
+ }
+
+ companion object {
+ const val ACTION_DOWNLOAD_MODULE = 0
+ const val ACTION_CONTINUE_MODULE = 1
+ const val ACTION_MODULE_IMPORT_COMPLETED =
+ "com.google.android.chimera.component.MODULE_IMPORT_COMPLETED"
+ const val ACTION_DOWNLOAD_TARGET_SELECTED =
+ "com.google.android.chimera.component.DOWNLOAD_TARGET_SELECTED"
+ const val EXTRA_REQUESTED_FEATURE_NAMES =
+ "com.google.android.chimera.component.REQUESTED_FEATURE_NAMES"
+ const val EXTRA_REQUESTED_FEATURE_VERSIONS =
+ "com.google.android.chimera.component.REQUESTED_FEATURE_VERSIONS"
+ const val EXTRA_REQUEST_ID =
+ "com.google.android.chimera.component.REQUEST_ID"
+ const val EXTRA_AFTER_AUTHORIZATION_ACTION =
+ "com.google.android.chimera.component.AFTER_AUTHORIZATION_ACTION"
+ const val EXTRA_CONTAINER_COMPONENT_CLASS_NAME =
+ "com.google.android.chimera.component.CONTAINER_COMPONENT_CLASS_NAME"
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleImportCompletionReceiver.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleImportCompletionReceiver.kt
new file mode 100644
index 0000000000..96744f7dd1
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleImportCompletionReceiver.kt
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.app.ActivityManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import com.google.android.chimera.config.ModuleDownloadRegistry
+
+/** Restores the pending request task after its requested module has been imported. */
+class ModuleImportCompletionReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == ModuleDownloadActivity.ACTION_DOWNLOAD_TARGET_SELECTED) {
+ val requestId = intent.getStringExtra(ModuleDownloadActivity.EXTRA_REQUEST_ID).orEmpty()
+ if (!PendingModuleRequestStore.markDownloadTargetSelected(context, requestId)) {
+ Log.w(TAG, "Ignoring chooser callback for unknown request")
+ }
+ return
+ }
+ if (intent.action != ModuleDownloadActivity.ACTION_MODULE_IMPORT_COMPLETED) return
+ val pending = PendingModuleRequestStore.loadAll(context).firstOrNull { request ->
+ val modules = ModuleDownloadRegistry.resolveModules(request.features)
+ val currentModule = modules.firstOrNull { it.catalogId == request.currentCatalogId }
+ ?: return@firstOrNull false
+ val componentForModule = request.componentClassName?.takeIf {
+ modules.size == 1 || it in currentModule.requiredComponentClassNames
+ }
+ ModuleDownloadRegistry.isModuleInstalled(
+ context,
+ currentModule,
+ request.features,
+ componentForModule,
+ )
+ } ?: return
+ if (pending.taskId < 0) {
+ PendingModuleRequestStore.remove(context, pending.requestId)
+ return
+ }
+ runCatching {
+ (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
+ .moveTaskToFront(pending.taskId, 0)
+ }.onFailure {
+ Log.w(TAG, "Unable to restore pending module task", it)
+ PendingModuleRequestStore.remove(context, pending.requestId)
+ }
+ }
+
+ private companion object {
+ const val TAG = "ChimeraImportReceiver"
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/component/PendingModuleRequestStore.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/PendingModuleRequestStore.kt
new file mode 100644
index 0000000000..a7a2168a55
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/component/PendingModuleRequestStore.kt
@@ -0,0 +1,170 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.component
+
+import android.content.Context
+import android.os.Build
+import com.google.android.chimera.config.ModuleDownloadRegistry
+import org.json.JSONArray
+import org.json.JSONObject
+
+/** Device-protected hand-off state used to recover download/import flows after the :ui process dies. */
+object PendingModuleRequestStore {
+ private const val PREFS = "chimera_pending_module_request"
+ private const val KEY_REQUESTS = "requests"
+ private const val MAX_REQUEST_AGE_MILLIS = 24L * 60 * 60 * 1000
+ private const val MAX_FUTURE_SKEW_MILLIS = 5L * 60 * 1000
+
+ data class PendingRequest(
+ val requestId: String,
+ val features: List,
+ val currentCatalogId: String,
+ val componentClassName: String?,
+ val afterAuthorizationAction: Int,
+ val taskId: Int,
+ val downloadTargetSelected: Boolean = false,
+ val createdAtMillis: Long = System.currentTimeMillis(),
+ )
+
+ @Synchronized
+ fun save(context: Context, request: PendingRequest) {
+ if (request.requestId.isEmpty() || request.features.isEmpty()) return
+ val requests = readValidRequests(context)
+ .filterNot { it.requestId == request.requestId }
+ .plus(request)
+ writeRequests(context, requests)
+ }
+
+ @Synchronized
+ fun load(context: Context, requestId: String): PendingRequest? {
+ if (requestId.isEmpty()) return null
+ return readValidRequests(context).firstOrNull { it.requestId == requestId }
+ }
+
+ @Synchronized
+ fun loadAll(context: Context): List {
+ return readValidRequests(context).sortedByDescending(PendingRequest::createdAtMillis)
+ }
+
+ @Synchronized
+ fun markDownloadTargetSelected(context: Context, requestId: String): Boolean {
+ if (requestId.isEmpty()) return false
+ var found = false
+ val requests = readValidRequests(context).map { request ->
+ if (request.requestId == requestId) {
+ found = true
+ request.copy(downloadTargetSelected = true)
+ } else {
+ request
+ }
+ }
+ if (found) writeRequests(context, requests)
+ return found
+ }
+
+ @Synchronized
+ fun remove(context: Context, requestId: String) {
+ if (requestId.isEmpty()) return
+ writeRequests(context, readValidRequests(context).filterNot { it.requestId == requestId })
+ }
+
+ private fun readValidRequests(context: Context): List {
+ val prefs = preferences(context)
+ val encoded = prefs.getString(KEY_REQUESTS, null) ?: run {
+ // Purge the legacy single-request schema when upgrading.
+ if (prefs.all.isNotEmpty()) prefs.edit().clear().commit()
+ return emptyList()
+ }
+ val now = System.currentTimeMillis()
+ val requests = runCatching {
+ val array = JSONArray(encoded)
+ buildList {
+ for (index in 0 until array.length()) {
+ decodeRequest(array.optJSONObject(index) ?: continue)?.let(::add)
+ }
+ }
+ }.getOrElse {
+ prefs.edit().clear().commit()
+ return emptyList()
+ }
+ val valid = requests.filter { request ->
+ request.createdAtMillis > 0L &&
+ request.createdAtMillis <= now + MAX_FUTURE_SKEW_MILLIS &&
+ now - request.createdAtMillis <= MAX_REQUEST_AGE_MILLIS
+ }
+ if (valid.size != requests.size) writeRequests(context, valid)
+ return valid
+ }
+
+ private fun decodeRequest(value: JSONObject): PendingRequest? {
+ val requestId = value.optString("requestId").takeIf(String::isNotEmpty) ?: return null
+ val featureArray = value.optJSONArray("features") ?: return null
+ val features = buildList {
+ for (index in 0 until featureArray.length()) {
+ val feature = featureArray.optJSONObject(index) ?: continue
+ val name = feature.optString("name").takeIf(String::isNotEmpty) ?: continue
+ add(
+ ModuleDownloadRegistry.RequestedFeature(
+ name = name,
+ minVersion = feature.optLong("minVersion", 0L),
+ )
+ )
+ }
+ }
+ if (features.isEmpty()) return null
+ return PendingRequest(
+ requestId = requestId,
+ features = features,
+ currentCatalogId = value.optString("currentCatalogId"),
+ componentClassName = value.optString("componentClassName")
+ .takeIf(String::isNotEmpty),
+ afterAuthorizationAction = value.optInt(
+ "afterAuthorizationAction",
+ ModuleDownloadActivity.ACTION_DOWNLOAD_MODULE,
+ ),
+ taskId = value.optInt("taskId", -1),
+ downloadTargetSelected = value.optBoolean("downloadTargetSelected", false),
+ createdAtMillis = value.optLong("createdAtMillis", 0L),
+ )
+ }
+
+ private fun writeRequests(context: Context, requests: List) {
+ val prefs = preferences(context)
+ if (requests.isEmpty()) {
+ prefs.edit().clear().commit()
+ return
+ }
+ val array = JSONArray()
+ requests.forEach { request ->
+ val features = JSONArray()
+ request.features.forEach { feature ->
+ features.put(
+ JSONObject()
+ .put("name", feature.name)
+ .put("minVersion", feature.minVersion)
+ )
+ }
+ array.put(
+ JSONObject()
+ .put("requestId", request.requestId)
+ .put("features", features)
+ .put("currentCatalogId", request.currentCatalogId)
+ .put("componentClassName", request.componentClassName.orEmpty())
+ .put("afterAuthorizationAction", request.afterAuthorizationAction)
+ .put("taskId", request.taskId)
+ .put("downloadTargetSelected", request.downloadTargetSelected)
+ .put("createdAtMillis", request.createdAtMillis)
+ )
+ }
+ prefs.edit().clear().putString(KEY_REQUESTS, array.toString()).commit()
+ }
+
+ private fun preferences(context: Context) =
+ (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ context.createDeviceProtectedStorageContext()
+ } else {
+ context
+ }).getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraApkManifestReader.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraApkManifestReader.kt
new file mode 100644
index 0000000000..200a527f38
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraApkManifestReader.kt
@@ -0,0 +1,106 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.Context
+import android.util.Log
+import java.io.File
+import java.io.InputStream
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.zip.ZipFile
+
+/** Authoritative identity of a Chimera module APK, read from assets/ChimeraManifest.pb. */
+data class ChimeraApkIdentity(
+ val moduleId: String?,
+ val moduleVersion: Int?,
+)
+
+/**
+ * Shared reader for the APK-local assets/ChimeraManifest.pb format.
+ *
+ * Google Chimera module APKs store a 4-byte big-endian length followed by a ChimeraManifest protobuf.
+ * Keep this parsing in one place so import, config registration, cleanup, and Provider disk fallback use the
+ * same length checks and malformed-APK behavior.
+ */
+object ChimeraApkManifestReader {
+private const val TAG = "ChimeraManifestReader"
+ private const val ENTRY_CHIMERA_MANIFEST = "assets/ChimeraManifest.pb"
+ private const val MAX_CHIMERA_MANIFEST_BYTES = 1024L * 1024
+
+ fun readManifest(apkFile: File): ChimeraManifest? {
+ return try {
+ ZipFile(apkFile).use { zip ->
+ val entry = zip.getEntry(ENTRY_CHIMERA_MANIFEST) ?: return null
+ zip.getInputStream(entry).use { input ->
+ val header = ByteArray(4)
+ if (readFully(input, header) != 4) return null
+ val size = ByteBuffer.wrap(header).order(ByteOrder.BIG_ENDIAN).int
+ if (size <= 0 || size > MAX_CHIMERA_MANIFEST_BYTES) return null
+ val body = ByteArray(size)
+ if (readFully(input, body) != size) return null
+ ChimeraManifest.ADAPTER.decode(body)
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "readManifest failed for ${apkFile.name}: ${e.message}")
+ null
+ }
+ }
+
+ /** Returns ALL module manifests bundled in the APK; one APK can expose several Chimera module IDs. */
+ fun readModuleManifests(apkFile: File): List? =
+ readManifest(apkFile)?.chimeraModuleManifests
+
+ fun readCapabilities(apkFile: File): List =
+ readModuleManifests(apkFile).orEmpty().mapNotNull { manifest ->
+ val moduleId = manifest.moduleId?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null
+ val moduleVersion = manifest.moduleVersion?.takeIf { it > 0 } ?: return@mapNotNull null
+ ChimeraModuleCapabilities(
+ moduleId = moduleId,
+ moduleVersion = moduleVersion,
+ initializerMode = InitializerMode.fromRequiredApis(manifest.requiredApis),
+ requiredApis = manifest.requiredApis,
+ activityBindings = manifest.activityBindings,
+ boundServiceBindings = manifest.boundServiceBindings,
+ providerBindings = manifest.providerBindings,
+ sliceProviderBindings = manifest.sliceProviderBindings,
+ )
+ }
+
+ /** Returns capabilities only when the configured artifact still passes the persisted hash check. */
+ fun readVerifiedCapabilities(
+ context: Context,
+ chimeraModule: ChimeraModule,
+ ): List {
+ val installedApkPath = chimeraModule.installedApkPath?.takeIf { it.isNotEmpty() } ?: return emptyList()
+ val verifiedApk = ChimeraStorage.verifiedModuleApk(
+ context = context,
+ file = File(installedApkPath),
+ expectedModuleName = null,
+ expectedSha256 = chimeraModule.apkSha256,
+ ) ?: return emptyList()
+ return readCapabilities(verifiedApk)
+ }
+
+ /**
+ * Returns every identity carried by an APK. Chimera APKs commonly expose several module IDs from one
+ * signed artifact, so callers must not collapse the artifact to the first manifest for version decisions.
+ */
+ fun readIdentities(apkFile: File): List =
+ readModuleManifests(apkFile).orEmpty().map {
+ ChimeraApkIdentity(it.moduleId, it.moduleVersion)
+ }
+
+ private fun readFully(input: InputStream, buf: ByteArray): Int {
+ var off = 0
+ while (off < buf.size) {
+ val r = input.read(buf, off, buf.size - off)
+ if (r < 0) break
+ off += r
+ }
+ return off
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraConfigManager.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraConfigManager.kt
new file mode 100644
index 0000000000..9a42278745
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraConfigManager.kt
@@ -0,0 +1,546 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.Context
+import android.os.StrictMode
+import android.util.Log
+import com.google.android.chimera.config.registry.DynamicModuleRegistry
+import com.google.android.gms.chimera.ModuleInfo
+import com.google.android.gms.common.app.AppContext
+import org.microg.gms.common.Constants
+import java.io.File
+import java.io.IOException
+import java.io.RandomAccessFile
+import java.util.UUID
+import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.concurrent.read
+import kotlin.concurrent.write
+import androidx.core.net.toUri
+// Storage path (CE/DE) decisions must use the real system SDK: microG Profile's spoofed Build
+// returns SDK_INT < 24 in processes where the Profile isn't active (e.g. :ui), which disables the
+// forced-DE branch and falls back to CE, causing the main process (DE) and :ui (CE) to read/write
+// different chimera_manifest.pb files (cross-process inconsistency in listing/deletion).
+import android.os.Build
+
+data class RemovedModuleMetadata(
+ val moduleIds: Set = emptySet(),
+ val apkPaths: Set = emptySet(),
+ val persisted: Boolean = true,
+)
+
+object ChimeraConfigManager {
+
+ private const val TAG = "ChimeraConfigManager"
+
+ private var configFile: File? = null
+ private var currentConfig: ChimeraManifestStore? = null
+ private var loadedConfigLastModified = Long.MIN_VALUE
+ private var loadedConfigLength = Long.MIN_VALUE
+ private val lock = ReentrantReadWriteLock()
+
+ @JvmStatic
+ private fun getChimeraManifest(): ChimeraManifestStore {
+ lock.read {
+ currentConfig?.let { cached ->
+ val file = configFile
+ if (file == null ||
+ (file.lastModified() == loadedConfigLastModified && file.length() == loadedConfigLength)
+ ) return cached
+ }
+ }
+
+ lock.write {
+ currentConfig?.let { cached ->
+ val file = configFile
+ if (file == null ||
+ (file.lastModified() == loadedConfigLastModified && file.length() == loadedConfigLength)
+ ) return cached
+ }
+
+ if (!AppContext.isInitialized()) {
+ // Don't cache into currentConfig: otherwise configFile is never set and later saveToFile silently loses writes
+ return ChimeraManifestStore()
+ }
+
+ val context = AppContext.get()
+ val ctx = if (Build.VERSION.SDK_INT >= 24 &&
+ !context.isDeviceProtectedStorage
+ ) {
+ context.createDeviceProtectedStorageContext()
+ } else {
+ context
+ }
+
+ val file = File(getChimeraDir(ctx), "chimera_manifest.pb")
+ configFile = file
+ Log.d(TAG, "getChimeraManifest: reading configFile=${file.path}")
+
+ currentConfig = try {
+ withConfigFileLock(file) { readConfigFile(file) }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to read ChimeraManifestStore: ${e.message}", e)
+ // Fail closed for later updates: updateConfig reads the file again under its cross-process lock
+ // and refuses to save when decoding still fails.
+ ChimeraManifestStore()
+ }
+ loadedConfigLastModified = file.lastModified()
+ loadedConfigLength = file.length()
+
+ return currentConfig!!
+ }
+ }
+
+ fun updateConfig(
+ autoSave: Boolean = true,
+ transform: (ChimeraManifestStore) -> ChimeraManifestStore
+ ): ChimeraManifestStore = lock.write {
+ if (!autoSave) {
+ val oldConfig = currentConfig ?: getChimeraManifest()
+ return@write runCatching { transform(oldConfig) }
+ .onSuccess { currentConfig = it }
+ .getOrElse {
+ Log.e(TAG, "Failed to update in-memory ChimeraManifestStore", it)
+ oldConfig
+ }
+ }
+
+ val file = resolveConfigFile()
+ if (file == null) {
+ Log.e(TAG, "Cannot update ChimeraManifestStore before AppContext initialization")
+ return@write currentConfig ?: ChimeraManifestStore()
+ }
+ return@write try {
+ withConfigFileLock(file) {
+ // Cross-process read-modify-write: never transform a stale process-local snapshot.
+ val diskConfig = readConfigFile(file)
+ val newConfig = transform(diskConfig)
+ if (!saveToFile(file, newConfig)) {
+ currentConfig = diskConfig
+ loadedConfigLastModified = file.lastModified()
+ loadedConfigLength = file.length()
+ diskConfig
+ } else {
+ currentConfig = newConfig
+ loadedConfigLastModified = file.lastModified()
+ loadedConfigLength = file.length()
+ newConfig
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Refusing to overwrite unreadable ChimeraManifestStore: ${e.message}", e)
+ currentConfig ?: ChimeraManifestStore()
+ }
+ }
+
+ private fun saveToFile(file: File, config: ChimeraManifestStore): Boolean {
+ val tmpFile = File(file.parentFile, "${file.name}.tmp-${UUID.randomUUID()}")
+ try {
+ StrictMode.allowThreadDiskWrites().use {
+ tmpFile.outputStream().use { ChimeraManifestStore.ADAPTER.encode(it, config) }
+ }
+ if (!tmpFile.renameTo(file)) {
+ throw IOException("Failed to overwrite config file: ${file.path}")
+ }
+ Log.d(TAG, "saveToFile: persisted ${config.chimeraModules.size} modules to ${file.path}")
+ return true
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to save ChimeraManifestStore: ${e.message}", e)
+ return false
+ } finally {
+ tmpFile.delete()
+ }
+ }
+
+ fun reload(): ChimeraManifestStore = lock.write {
+ currentConfig = null
+ loadedConfigLastModified = Long.MIN_VALUE
+ loadedConfigLength = Long.MIN_VALUE
+ getChimeraManifest()
+ }
+
+ fun getConfigFile(context: Context): File = File(getChimeraDir(context), "chimera_manifest.pb")
+
+ fun getConfigLastModified(context: Context): Long = getConfigFile(context).takeIf { it.isFile }?.lastModified() ?: 0L
+
+ private fun getChimeraDir(context: Context): File {
+ val deviceProtectedContext = if (Build.VERSION.SDK_INT >= 24 && !context.isDeviceProtectedStorage) {
+ context.createDeviceProtectedStorageContext()
+ } else {
+ context
+ }
+ val oldPolicy = StrictMode.allowThreadDiskWrites()
+ try {
+ return deviceProtectedContext.getDir("chimera", Context.MODE_PRIVATE)
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy)
+ }
+ }
+
+ private inline fun StrictMode.ThreadPolicy.use(block: () -> T): T {
+ val old = StrictMode.getThreadPolicy()
+ StrictMode.setThreadPolicy(this)
+ return try {
+ block()
+ } finally {
+ StrictMode.setThreadPolicy(old)
+ }
+ }
+
+ fun findChimeraBoundService(serviceName: String): ComponentRoute? {
+ val targetName = serviceName.removePrefix(Constants.GMS_PACKAGE_NAME)
+ return getChimeraManifest()
+ .collections
+ ?.serviceRoutes
+ ?.find { it.containerName == targetName }
+ }
+
+ private fun findRoute(currentClassName: String): ComponentRoute? {
+ val manifest = getChimeraManifest()
+ val routes = manifest.collections?.activeRoutes ?: return null
+
+ val variants = linkedSetOf()
+ variants += currentClassName
+
+ val chimeraPrefix = getChimeraPrefix()
+ if (chimeraPrefix.isNotEmpty()) {
+ variants += currentClassName.removePrefix(chimeraPrefix)
+ }
+
+ val gmsPkg = Constants.GMS_PACKAGE_NAME
+ if (currentClassName.startsWith("$gmsPkg.")) {
+ val suffix = currentClassName.removePrefix(gmsPkg)
+ variants += suffix
+ variants += if (suffix.startsWith(".")) suffix else ".$suffix"
+ variants += currentClassName.removePrefix("$gmsPkg.")
+ }
+
+ return routes.find { route -> route.containerName in variants }
+ }
+
+ fun findComponentByComponentName(currentClassName: String): ComponentRoute? =
+ findRoute(currentClassName)
+
+ fun findModuleByComponent(currentClassName: String): ChimeraModule? {
+ val route = findRoute(currentClassName) ?: return null
+ return getChimeraManifest().chimeraModules.find { it.moduleId == route.moduleId }
+ }
+ fun findModuleByModuleId(moduleId: String): ChimeraModule? {
+ return getChimeraManifest().chimeraModules.find { it.moduleId == moduleId }
+ }
+
+ fun findModuleByModuleName(moduleName: String): ChimeraModule? {
+ return getChimeraManifest().chimeraModules.find { it.moduleName == moduleName }
+ }
+
+ /** Returns the module identities persisted from successfully imported Chimera artifacts. */
+ fun getRegisteredModules(): List = getChimeraManifest().chimeraModules
+
+ private fun resolveConfigFile(): File? {
+ configFile?.let { return it }
+ if (!AppContext.isInitialized()) return null
+ return File(getChimeraDir(AppContext.get()), "chimera_manifest.pb").also { configFile = it }
+ }
+
+ private fun readConfigFile(file: File): ChimeraManifestStore {
+ if (!file.exists()) return ChimeraManifestStore()
+ return StrictMode.allowThreadDiskReads().use {
+ file.inputStream().use { ChimeraManifestStore.ADAPTER.decode(it) }
+ }
+ }
+
+ private inline fun withConfigFileLock(file: File, block: () -> T): T {
+ file.parentFile?.mkdirs()
+ val lockFile = File(file.parentFile, "${file.name}.lock")
+ return RandomAccessFile(lockFile, "rw").channel.use { channel ->
+ channel.lock().use { block() }
+ }
+ }
+
+ fun isApkPathReferenced(apkPath: String): Boolean {
+ if (apkPath.isEmpty()) return false
+ return getChimeraManifest().chimeraModules.any { it.installedApkPath == apkPath }
+ }
+
+ /** Resolve a module by its moduleId, falling back to moduleName — the loader/manager standard two-step lookup. */
+ fun findModule(moduleId: String, moduleName: String): ChimeraModule? =
+ findModuleByModuleId(moduleId) ?: findModuleByModuleName(moduleName)
+
+ /**
+ * Resolve an installed module by moduleName, ignoring stale placeholder entries that carry a
+ * non-positive version. A feature sub-moduleId (e.g. mlkit_docscan_detect) has no config entry of its
+ * own and the loader may persist a placeholder for it with version 0; this picks the highest valid
+ * version so a version lookup for a sub-moduleId reports the real parent-module version.
+ */
+ fun findInstalledModuleByName(moduleName: String): ChimeraModule? {
+ return getChimeraManifest().chimeraModules
+ .filter {
+ it.moduleName == moduleName && !it.installedApkPath.isNullOrEmpty() &&
+ (it.moduleVersion?.toIntOrNull() ?: 0) > 0
+ }
+ .maxByOrNull { it.moduleVersion?.toIntOrNull() ?: 0 }
+ }
+
+ /** List of (moduleName, moduleVersion) for registered (imported and persisted) modules, for UI display. For cross-process reads call [reload] first. */
+ fun listInstalledModules(): List> {
+ val mods = getChimeraManifest().chimeraModules
+ Log.d(TAG, "listInstalledModules: ${mods.size} modules in config, AppContext.init=${AppContext.isInitialized()}")
+ return mods
+ .filter { !it.moduleName.isNullOrEmpty() }
+ .map { it.moduleName!! to it.moduleVersion.orEmpty() }
+ .distinct()
+ }
+
+ /** Returns settings-safe capability status for the latest registered version of each module name. */
+ fun listInstalledModuleStatuses(context: Context): List =
+ getChimeraManifest().chimeraModules
+ .filter { !it.moduleName.isNullOrEmpty() }
+ .groupBy { it.moduleName!! }
+ .map { (moduleName, entries) ->
+ val module = entries.maxByOrNull { it.moduleVersion?.toLongOrNull() ?: 0L }!!
+ val capability = module.moduleId?.let { moduleId ->
+ ChimeraApkManifestReader.readVerifiedCapabilities(context, module)
+ .filter { it.moduleId == moduleId }
+ }.orEmpty()
+ InstalledModuleStatus(
+ moduleName = moduleName,
+ moduleVersion = module.moduleVersion.orEmpty(),
+ capabilityStatus = ChimeraCapabilitySupport.classify(context, capability),
+ )
+ }
+
+ fun featureConfigByKey(key: String?): FeatureDescriptor? {
+ if (key == null) {
+ return null
+ }
+
+ val result = getChimeraManifest().featureDescriptors.find { it.featureName == key }
+ if (result == null) {
+ Log.d(TAG, "featureConfigByKey($key): NOT FOUND (total features: ${getChimeraManifest().featureDescriptors.size})")
+ }
+ return result
+ }
+
+ fun getChimeraPrefix(): String {
+ return getChimeraManifest().collections?.chimeraClassNamePrefix ?: ""
+ }
+
+ /**
+ * Registers downloaded artifacts by identities from their signed ChimeraManifest.pb. The immutable .mods
+ * mapping name is intentionally not an ownership key: known IDs use [DynamicModuleRegistry]'s canonical
+ * name and unknown future IDs use the signed moduleId itself.
+ */
+ fun updateModuleDownload(moduleInfos: List): ChimeraManifestStore {
+ check(AppContext.isInitialized() && DynamicModuleSettings.isAvailable(AppContext.get())) {
+ "Dynamic modules are unavailable on this device or disabled by the user"
+ }
+ return updateConfig(autoSave = true) { oldConfig ->
+ val newConfig = oldConfig.newBuilder()
+ val newCollections = oldConfig.collections?.newBuilder() ?: ChimeraModuleCollections.Builder()
+ val modules = oldConfig.chimeraModules.map { it.newBuilder().build() }.toMutableList()
+
+ for (moduleInfo in moduleInfos) {
+ val apkPath = moduleInfo.source?.toUri()?.path.orEmpty()
+ val manifests = readApkChimeraManifests(apkPath)
+ if (apkPath.isEmpty() || manifests.isNullOrEmpty()) {
+ throw IllegalArgumentException(
+ "Artifact has no readable ChimeraManifest: ${moduleInfo.source}"
+ )
+ }
+ require(manifests.all { !it.moduleId.isNullOrEmpty() && (it.moduleVersion ?: 0) > 0 }) {
+ "Artifact contains an invalid Chimera identity: ${moduleInfo.source}"
+ }
+
+ for (manifest in manifests) {
+ val moduleId = manifest.moduleId?.takeIf { it.isNotEmpty() } ?: continue
+ val moduleName = DynamicModuleRegistry.canonicalModuleName(moduleId)
+ val registeredVersion = manifest.moduleVersion
+ ?.takeIf { it > 0 }
+ ?.toString()
+ ?: moduleInfo.module_version
+ val incomingVersion = registeredVersion?.toLongOrNull()
+ val index = modules.indexOfFirst { it.moduleId == moduleId }
+ val oldEntry = index.takeIf { it >= 0 }?.let(modules::get)
+ val oldVersion = oldEntry?.moduleVersion?.toLongOrNull()
+ if (oldVersion != null && incomingVersion != null && oldVersion > incomingVersion) {
+ Log.i(TAG, "Keeping newer installed module $moduleId v$oldVersion over v$incomingVersion")
+ continue
+ }
+
+ val oldManifest = oldEntry?.installedApkPath
+ ?.takeIf { it.isNotEmpty() }
+ ?.let(::readApkChimeraManifests)
+ .orEmpty()
+ .firstOrNull { it.moduleId == moduleId }
+ val oldFeatures = oldManifest?.featureDescriptors.orEmpty()
+ .mapNotNull { it.featureName?.takeIf(String::isNotEmpty) }
+ .toSet()
+ val replacementFeatures = manifest.featureDescriptors
+ .mapNotNull { it.featureName?.takeIf(String::isNotEmpty) }
+ .toSet()
+ val featuresOwnedElsewhere = featureNamesFromInstalledEntries(
+ modules.filter { it.moduleId != moduleId }
+ )
+ val removableFeatures = oldFeatures - replacementFeatures - featuresOwnedElsewhere
+ if (removableFeatures.isNotEmpty()) {
+ newConfig.featureDescriptors = newConfig.featureDescriptors.filterNot {
+ it.featureName in removableFeatures
+ }
+ }
+
+ val replacement = (oldEntry?.newBuilder() ?: ChimeraModule.Builder()).apply {
+ this.moduleId = moduleId
+ this.moduleName = moduleName
+ moduleVersion = registeredVersion
+ installedApkPath = apkPath
+ // The APK-local signed manifest declares the host ModuleApi. Do not infer it
+ // from component bindings: a module can contain several initializer classes.
+ moduleApiClassname = manifest.requiredApis.orEmpty()
+ apkSha256 = moduleInfo.sha256_hash
+ }.build()
+ if (index >= 0) modules[index] = replacement else modules.add(replacement)
+
+ if (newCollections.chimeraClassNamePrefix.isNullOrEmpty()) {
+ newCollections.chimeraClassNamePrefix = manifest.chimeraClassNamePrefix
+ }
+ newCollections.activeRoutes = newCollections.activeRoutes.filterNot { it.moduleId == moduleId }
+ newCollections.serviceRoutes = newCollections.serviceRoutes.filterNot { it.moduleId == moduleId }
+ newCollections.activeRoutes += manifest.activityBindings.map {
+ ComponentRoute.build {
+ containerName = it.containerName
+ moduleChimeraName = it.moduleChimeraName
+ this.moduleId = moduleId
+ }
+ }
+ newCollections.serviceRoutes += manifest.boundServiceBindings.map {
+ ComponentRoute.build {
+ containerName = it.containerName
+ moduleChimeraName = it.moduleChimeraName
+ this.moduleId = moduleId
+ }
+ }
+ manifest.featureDescriptors.forEach { descriptor ->
+ newConfig.featureDescriptors = newConfig.featureDescriptors.filterNot {
+ it.featureName == descriptor.featureName
+ }
+ newConfig.featureDescriptors += FeatureDescriptor.build {
+ featureName = descriptor.featureName
+ featureVersion = descriptor.featureVersion
+ }
+ }
+ }
+ }
+
+ newConfig.chimeraModules = modules
+ newConfig.collections(newCollections.build())
+ newConfig.build()
+ }
+ }
+
+ fun removeModuleMetadata(moduleName: String, apkPathHint: String? = null): RemovedModuleMetadata {
+ var removed = RemovedModuleMetadata()
+ val updatedConfig = updateConfig(autoSave = true) { oldConfig ->
+ val newConfig = oldConfig.newBuilder()
+ val newCollections = oldConfig.collections?.newBuilder() ?: ChimeraModuleCollections.Builder()
+ val newChimeraModules = oldConfig.chimeraModules.map { it.newBuilder().build() }.toMutableList()
+
+ removed = cleanupModuleMetadata(
+ newConfig,
+ newCollections,
+ newChimeraModules,
+ moduleName,
+ replacementManifests = emptyList(),
+ apkPathHints = listOfNotNull(apkPathHint)
+ )
+
+ newConfig.chimeraModules = newChimeraModules
+ newConfig.collections(newCollections.build())
+ newConfig.build()
+ }
+ return if (updatedConfig.chimeraModules.any { it.moduleName == moduleName }) {
+ Log.e(TAG, "Module metadata removal was not persisted for $moduleName")
+ removed.copy(persisted = false)
+ } else {
+ removed
+ }
+ }
+
+ private fun cleanupModuleMetadata(
+ newConfig: ChimeraManifestStore.Builder,
+ newCollections: ChimeraModuleCollections.Builder,
+ newChimeraModules: MutableList,
+ moduleName: String,
+ replacementManifests: List,
+ apkPathHints: List = emptyList()
+ ): RemovedModuleMetadata {
+ if (moduleName.isEmpty()) return RemovedModuleMetadata()
+
+ val oldEntries = newChimeraModules.filter { it.moduleName == moduleName }
+ val oldPaths = (oldEntries.mapNotNull { it.installedApkPath?.takeIf { path -> path.isNotEmpty() } } +
+ apkPathHints.filter { it.isNotEmpty() }).distinct()
+ val oldModuleIds = oldEntries.mapNotNull { it.moduleId?.takeIf { id -> id.isNotEmpty() } }.toSet()
+ val oldManifests = oldPaths.flatMap { path ->
+ readApkChimeraManifests(path).orEmpty().filter { it.moduleId in oldModuleIds }
+ }
+
+ if (oldEntries.isEmpty() && oldModuleIds.isEmpty() && oldPaths.isEmpty()) {
+ return RemovedModuleMetadata()
+ }
+
+ val replacementModuleIds = replacementManifests.mapNotNull { it.moduleId?.takeIf { id -> id.isNotEmpty() } }.toSet()
+ val staleModuleIds = if (replacementModuleIds.isEmpty()) oldModuleIds else oldModuleIds - replacementModuleIds
+
+ if (replacementModuleIds.isEmpty()) {
+ newChimeraModules.removeAll { it.moduleName == moduleName }
+ } else {
+ newChimeraModules.removeAll { it.moduleName == moduleName && it.moduleId !in replacementModuleIds }
+ }
+
+ if (staleModuleIds.isNotEmpty()) {
+ newCollections.activeRoutes = newCollections.activeRoutes.filterNot { it.moduleId in staleModuleIds }
+ newCollections.serviceRoutes = newCollections.serviceRoutes.filterNot { it.moduleId in staleModuleIds }
+ }
+
+ val oldFeatureNames = featureNamesFromManifests(oldManifests)
+ if (oldFeatureNames.isNotEmpty()) {
+ val replacementFeatureNames = featureNamesFromManifests(replacementManifests)
+ val otherInstalledFeatureNames = featureNamesFromInstalledEntries(
+ newChimeraModules.filter { it.moduleName != moduleName }
+ )
+ val removableFeatureNames = oldFeatureNames - replacementFeatureNames - otherInstalledFeatureNames
+ if (removableFeatureNames.isNotEmpty()) {
+ newConfig.featureDescriptors = newConfig.featureDescriptors.filterNot {
+ it.featureName in removableFeatureNames
+ }
+ Log.d(TAG, "Removed stale feature descriptors for $moduleName: $removableFeatureNames")
+ }
+ }
+
+ return RemovedModuleMetadata(oldModuleIds, oldPaths.toSet())
+ }
+
+ private fun featureNamesFromInstalledEntries(entries: Iterable): Set {
+ val idsByPath = entries
+ .mapNotNull { entry ->
+ val path = entry.installedApkPath?.takeIf(String::isNotEmpty) ?: return@mapNotNull null
+ val moduleId = entry.moduleId?.takeIf(String::isNotEmpty) ?: return@mapNotNull null
+ path to moduleId
+ }
+ .groupBy({ it.first }, { it.second })
+ return idsByPath.flatMap { (path, moduleIds) ->
+ readApkChimeraManifests(path).orEmpty().filter { it.moduleId in moduleIds }
+ }.let(::featureNamesFromManifests)
+ }
+
+ private fun featureNamesFromManifests(manifests: Iterable): Set {
+ return manifests
+ .flatMap { it.featureDescriptors }
+ .mapNotNull { it.featureName?.takeIf { name -> name.isNotEmpty() } }
+ .toSet()
+ }
+
+ private fun readApkChimeraManifests(zipPath: String): List? =
+ ChimeraApkManifestReader.readModuleManifests(File(zipPath))
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleBootstrap.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleBootstrap.kt
new file mode 100644
index 0000000000..07652a2118
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleBootstrap.kt
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.Context
+import android.os.StrictMode
+import android.util.Log
+import com.google.android.chimera.config.registry.ContainerRouteRegistry
+import com.google.android.gms.common.app.AppContext
+import com.google.android.gms.common.app.GCoreApplicationContext
+
+object ChimeraModuleBootstrap {
+ private const val TAG = "ChimeraModuleBootstrap"
+
+ @Volatile
+ private var initialized = false
+
+ fun ensureInitialized(context: Context) {
+ if (initialized) return
+ synchronized(this) {
+ if (initialized) return
+ val oldPolicy = StrictMode.allowThreadDiskWrites()
+ try {
+ doInit(context)
+ initialized = true
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to initialize local modules", e)
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy)
+ }
+ }
+ }
+
+ private fun doInit(context: Context) {
+ if (!AppContext.isInitialized()) {
+ (context.applicationContext as? android.app.Application)?.let { AppContext.init(it) }
+ }
+
+ val appContext = context.applicationContext
+
+ val coreAppContext = GCoreApplicationContext.instance
+ if (coreAppContext.baseContext == null) {
+ coreAppContext.attachBaseContext(appContext)
+ }
+
+ val moduleDir = ChimeraStorage.ensureModuleRoot(appContext)
+
+ registerContainerBoundServices()
+
+ // An APK found on disk without a persisted config entry has no expected digest or verified
+ // import transaction to bind it to. Keep it as an orphan for explicit user cleanup/re-import;
+ // never turn a directory scan into a newly trusted executable module.
+ val orphanCount = ChimeraStorage.listDownloadedApks(moduleDir).count { apk ->
+ ChimeraConfigManager.findModuleByModuleName(apk.moduleName) == null
+ }
+ if (orphanCount != 0) {
+ Log.w(TAG, "Ignoring $orphanCount unregistered Chimera module APK(s)")
+ }
+ }
+
+ private fun registerContainerBoundServices() {
+ ChimeraConfigManager.updateConfig(autoSave = true) { config ->
+ val collections = config.collections?.newBuilder() ?: ChimeraModuleCollections.Builder()
+ var changed = false
+
+ val existingServices = collections.serviceRoutes.map { it.containerName }.toSet()
+ val newRoutes = ContainerRouteRegistry.serviceRoutes.filter { it.containerName !in existingServices }
+ if (newRoutes.isNotEmpty()) {
+ Log.i(TAG, "Registering ${newRoutes.size} container service route(s)")
+ collections.serviceRoutes += newRoutes
+ changed = true
+ }
+
+ if (!changed) return@updateConfig config
+ config.newBuilder().collections(collections.build()).build()
+ }
+ }
+
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleCapabilities.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleCapabilities.kt
new file mode 100644
index 0000000000..de099789ee
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleCapabilities.kt
@@ -0,0 +1,95 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.ComponentName
+import android.content.Context
+import org.microg.gms.common.Constants
+
+/** The host API requested by a signed Chimera module manifest. */
+enum class InitializerMode(val moduleApiClassName: String?) {
+ DYNAMITE("com.google.android.gms.chimera.container.DynamiteModuleApi"),
+ GMS("com.google.android.gms.chimera.container.GmsModuleApi"),
+ UNSUPPORTED(null);
+
+ companion object {
+ fun fromRequiredApis(requiredApis: String?): InitializerMode = entries.firstOrNull {
+ it.moduleApiClassName == requiredApis
+ } ?: UNSUPPORTED
+ }
+}
+
+/** Immutable capability data read from an APK-local signed Chimera manifest. */
+data class ChimeraModuleCapabilities(
+ val moduleId: String,
+ val moduleVersion: Int,
+ val initializerMode: InitializerMode,
+ val requiredApis: String?,
+ val activityBindings: List,
+ val boundServiceBindings: List,
+ val providerBindings: List,
+ val sliceProviderBindings: List,
+)
+
+/** Host-visible support state derived from a verified signed module capability. */
+enum class ModuleCapabilityStatus {
+ LOADABLE,
+ PARTIAL_COMPONENT_SUPPORT,
+ UNSUPPORTED_INITIALIZER,
+ UNVERIFIED_ARTIFACT,
+}
+
+/** The module entry shown in the settings screen; it intentionally excludes APK paths and digests. */
+data class InstalledModuleStatus(
+ val moduleName: String,
+ val moduleVersion: String,
+ val capabilityStatus: ModuleCapabilityStatus,
+)
+
+/** Checks whether a signed component capability has a host component that Android can dispatch. */
+object ChimeraCapabilitySupport {
+ fun classify(context: Context, capabilities: List): ModuleCapabilityStatus {
+ if (capabilities.isEmpty()) return ModuleCapabilityStatus.UNVERIFIED_ARTIFACT
+ if (capabilities.any { it.initializerMode == InitializerMode.UNSUPPORTED }) {
+ return ModuleCapabilityStatus.UNSUPPORTED_INITIALIZER
+ }
+ if (capabilities.any { it.providerBindings.isNotEmpty() || it.sliceProviderBindings.isNotEmpty() }) {
+ return ModuleCapabilityStatus.PARTIAL_COMPONENT_SUPPORT
+ }
+ if (capabilities.any { capability ->
+ capability.activityBindings.any { !hasActivity(context, it.containerName) } ||
+ capability.boundServiceBindings.any { !hasService(context, it.moduleChimeraName) }
+ }
+ ) {
+ return ModuleCapabilityStatus.PARTIAL_COMPONENT_SUPPORT
+ }
+ return ModuleCapabilityStatus.LOADABLE
+ }
+
+ private fun hasActivity(context: Context, name: String?): Boolean =
+ hasComponent(context, name) { component ->
+ context.packageManager.getActivityInfo(component, 0)
+ }
+
+ private fun hasService(context: Context, name: String?): Boolean =
+ hasComponent(context, name) { component ->
+ context.packageManager.getServiceInfo(component, 0)
+ }
+
+ private fun hasComponent(
+ context: Context,
+ rawName: String?,
+ resolve: (ComponentName) -> Any,
+ ): Boolean {
+ val name = rawName?.toHostClassName() ?: return false
+ return runCatching { resolve(ComponentName(context, name)) }.isSuccess
+ }
+
+ private fun String.toHostClassName(): String = when {
+ startsWith('.') -> Constants.GMS_PACKAGE_NAME + this
+ startsWith("${Constants.GMS_PACKAGE_NAME}.") -> this
+ else -> "${Constants.GMS_PACKAGE_NAME}.$this"
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleInfoImpl.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleInfoImpl.kt
new file mode 100644
index 0000000000..44e422c3c8
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleInfoImpl.kt
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.os.Bundle
+import com.google.android.chimera.config.registry.DynamicModuleRegistry
+
+class ChimeraModuleInfoImpl(
+ dynamicModule: DynamicModuleRegistry.DynamicModule,
+ moduleApkInfo: ModuleManager.ModuleApkInfo?,
+ submoduleId: String?,
+ moduleVersion: Int,
+): ModuleManager.ModuleInfo(
+ dynamicModule.primaryModuleId,
+ moduleVersion,
+ submoduleId,
+ null,
+ moduleApkInfo) {
+
+ // microG has no module metadata source, so this is always empty (official callers only check key presence).
+ override fun getMetadata(): Bundle = Bundle.EMPTY
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleManager.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleManager.kt
new file mode 100644
index 0000000000..f373719220
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleManager.kt
@@ -0,0 +1,336 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import com.google.android.chimera.context.ModuleContext
+import com.google.android.chimera.loader.ChimeraModuleLdr
+import com.google.android.chimera.config.registry.DynamicModuleRegistry
+import com.google.android.chimera.config.registry.FeatureConfigRegistry
+import com.google.android.gms.common.app.AppContext
+
+class ChimeraModuleManager(
+ private val context: Context,
+ private val moduleContext: ModuleContext?,
+ private val hasModuleId: Boolean,
+) : ModuleManager() {
+ private var moduleInfo: ModuleInfo? = null
+ private var moduleApkInfo: ModuleApkInfo? = null
+
+ companion object {
+ private const val TAG = "ChimeraModuleManager"
+ }
+
+ private fun toModuleApkInfo(chimeraModule: ChimeraModule?) = ModuleApkInfo(
+ chimeraModule?.packageName ?: "com.google.android.gms",
+ chimeraModule?.versionName ?: "",
+ chimeraModule?.versionCode ?: 0,
+ chimeraModule?.sourceType ?: 0,
+ 0L,
+ false
+ )
+
+ private fun toDynamicModule(moduleId: String, chimeraModule: ChimeraModule?): DynamicModuleRegistry.DynamicModule {
+ return DynamicModuleRegistry.getByModuleId(moduleId)
+ ?: DynamicModuleRegistry.DynamicModule(
+ moduleName = chimeraModule?.moduleName?.takeIf { it.isNotEmpty() } ?: moduleId,
+ moduleIds = listOf(moduleId)
+ )
+ }
+
+ private fun toModuleInfo(
+ dynamicModule: DynamicModuleRegistry.DynamicModule,
+ chimeraModule: ChimeraModule?,
+ submoduleId: String?,
+ moduleVersion: Int = chimeraModule?.moduleVersion?.toIntOrNull() ?: 0,
+ ): ChimeraModuleInfoImpl = ChimeraModuleInfoImpl(
+ dynamicModule,
+ toModuleApkInfo(chimeraModule),
+ submoduleId,
+ moduleVersion,
+ )
+
+ override fun checkFeaturesAreAvailable(featureCheck: FeatureCheck): Int {
+ if (featureCheck.featureDescriptors.isEmpty()) {
+ Log.d(TAG, "No feature descriptors provided, returning FEATURE_CHECK_SUCCESS")
+ return FEATURE_CHECK_SUCCESS
+ }
+
+ return try {
+ Log.d(
+ TAG, "Checking ${featureCheck.featureDescriptors.size} feature(s): " +
+ featureCheck.featureDescriptors.joinToString { it.featureName ?: "\"\"" })
+ FeatureCheckUtils.checkFeatureDescriptors(
+ featureCheck.featureDescriptors,
+ allowStaticRegistry = true,
+ allowDynamicModules = DynamicModuleSettings.isAvailable(context)
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to retrieve available features: $e", e)
+ FEATURE_CHECK_ERROR
+ }
+ }
+
+ @Deprecated("This method is deprecated.")
+ override fun checkFeaturesAreAvailable(featureList: FeatureList): Int {
+ val protoBytes = featureList.getProtoBytes()
+ if (protoBytes == null || protoBytes.isEmpty()) {
+ return FEATURE_CHECK_SUCCESS
+ }
+
+ try {
+ return FeatureCheckUtils.checkFeatureListProto(
+ protoBytes,
+ allowDynamicModules = DynamicModuleSettings.isAvailable(context)
+ )
+ } catch (e: InvalidConfigException) {
+ Log.d(TAG, "Unable to retrieve available features: $e")
+ return FEATURE_CHECK_ERROR
+ }
+ }
+
+ override fun fetchFeatures(features: Array): FeatureList? {
+ if (features.isEmpty()) {
+ Log.e(TAG, "Feature check call didn't receive any featureNames")
+ return null
+ }
+ val dynamicModulesEnabled = DynamicModuleSettings.isAvailable(context)
+ val descriptors = features.mapNotNull { name ->
+ if (!dynamicModulesEnabled && ModuleDownloadRegistry.isKnownDynamicFeature(name)) {
+ return@mapNotNull null
+ }
+ val installed = if (dynamicModulesEnabled) {
+ ChimeraConfigManager.featureConfigByKey(name)
+ } else {
+ null
+ }
+ if (installed != null) {
+ FeatureDescriptor.Builder()
+ .featureName(name)
+ .featureVersion(installed.featureVersion ?: 0L)
+ .build()
+ } else {
+ val registry = FeatureConfigRegistry.featureMap[name]
+ if (registry != null) {
+ FeatureDescriptor.Builder()
+ .featureName(name)
+ .featureVersion(registry.featureVersion.toLong())
+ .build()
+ } else null
+ }
+ }
+ return if (descriptors.isNotEmpty()) FeatureList.fromDescriptors(descriptors) else null
+ }
+
+ override fun getAllModules(): Collection<*> {
+ val modules = linkedMapOf>()
+ for (entry in DynamicModuleRegistry.modules) {
+ if (entry.moduleName == "ROOT") continue // skip ROOT container
+ val chimeraModule = ChimeraConfigManager.findModule(entry.primaryModuleId, entry.moduleName)
+ modules[entry.primaryModuleId] = entry to chimeraModule
+ }
+ for (chimeraModule in ChimeraConfigManager.getRegisteredModules()) {
+ val moduleId = chimeraModule.moduleId?.takeIf { it.isNotEmpty() } ?: continue
+ val dynamicModule = toDynamicModule(moduleId, chimeraModule)
+ modules.getOrPut(dynamicModule.primaryModuleId) { dynamicModule to chimeraModule }
+ }
+ return modules.values.map { (dynamicModule, chimeraModule) ->
+ toModuleInfo(dynamicModule, chimeraModule, null)
+ }
+ }
+
+ override fun getAllModulesWithMetadata(metadataKey: String): Collection<*> {
+ return (getAllModules() as Collection).filter { moduleInfo ->
+ try {
+ moduleInfo.getMetadata().get(metadataKey) != null
+ } catch (_: Exception) {
+ false
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun getApiVersion(apiName: String): Int {
+ if (moduleContext == null) {
+ Log.d(TAG, "Unable to get current module\'s fulfilled APIs in ModuleManager created with non-module Context")
+ return -2
+ }
+ return moduleContext.getFulfilledApis().getOrDefault(apiName, -1)
+ }
+
+ override fun getCurrentConfig(): ConfigInfo? {
+ return ConfigInfo(emptyList(), (getAllModules() as Collection<*>).toList(), 0)
+ }
+
+ override fun getCurrentModule(): ModuleInfo? {
+ if (!hasModuleId) {
+ throw IllegalStateException("Unable to get current module info in ModuleManager created with non-module Context");
+ }
+
+ synchronized(this) {
+ if (moduleInfo == null) {
+ initializeModuleInfo()
+ }
+ return moduleInfo
+ }
+ }
+
+ override fun getCurrentModuleApk(): ModuleApkInfo? {
+ require(moduleContext != null) { "Unable to get current module APK info in ModuleManager created with non-module Context" }
+
+ synchronized(this) {
+ if (moduleApkInfo == null) {
+ initializeModuleInfo()
+ }
+ return moduleApkInfo
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun getThirdPartyLicenses(): java.util.Map<*, *> {
+ return java.util.HashMap() as java.util.Map<*, *>
+ }
+
+ override fun pauseModuleUpdates(moduleName: String, flags: Int) {
+ Log.d(TAG, "pauseModuleUpdates($moduleName, $flags) — not implemented")
+ }
+
+ override fun requestFeatures(request: FeatureRequest): Boolean {
+ Log.d(TAG, "requestFeatures: $request")
+ if (request.getRequestedFeatures().isEmpty()) {
+ // Feature release is currently bookkeeping-free, but it is safe and idempotent.
+ request.getListener()?.onRequestComplete(FEATURE_REQUEST_RESULT_SUCCESS)
+ return true
+ }
+ val check = FeatureCheck()
+ request.getRequestedFeatures().forEach { (feature, version) ->
+ check.checkFeatureAtVersion(feature, version)
+ }
+ val available = checkFeaturesAreAvailable(check) == FEATURE_CHECK_SUCCESS
+ request.getListener()?.onRequestComplete(
+ if (available) FEATURE_REQUEST_RESULT_SUCCESS else FEATURE_REQUEST_RESULT_FAILURE_NO_RETRY
+ )
+ if (!available) {
+ Log.w(TAG, "requestFeatures cannot complete synchronously; caller must use ModuleInstall UI")
+ }
+ return available
+ }
+
+ override fun resumeModuleUpdates(moduleName: String) {
+ Log.d(TAG, "resumeModuleUpdates($moduleName) — not implemented")
+ }
+
+ private fun initializeModuleInfo() {
+ require(moduleContext != null) { "Illegal state attempting to cache module info." }
+ val moduleId = moduleContext.getModuleId() ?: return
+ val registeredModule = ChimeraConfigManager.findModuleByModuleId(moduleId)
+ val dynamicModule = toDynamicModule(moduleId, registeredModule)
+ val chimeraModule = registeredModule
+ ?: ChimeraConfigManager.findModule(moduleId, dynamicModule.moduleName)
+ moduleApkInfo = toModuleApkInfo(chimeraModule)
+
+ if (hasModuleId) {
+ moduleInfo = toModuleInfo(
+ dynamicModule,
+ chimeraModule,
+ moduleContext.getSubmoduleId(),
+ moduleContext.getModuleVersion(),
+ )
+ }
+ }
+
+ class ChimeraModuleManagerSupplier : ModuleManagerSupplier {
+ companion object {
+ private const val TAG = "ModuleMgrSupplier"
+ }
+
+ override fun createModuleManager(context: Context): ModuleManager {
+ if (!AppContext.isInitialized()) {
+ Log.w(TAG, "AppContext not initialized, initializing with application context")
+ (context.applicationContext as? android.app.Application)?.let { AppContext.init(it) }
+ }
+ ChimeraModuleBootstrap.ensureInitialized(context)
+
+ ModuleContext.getModuleContext(context)?.let { existingContext ->
+ Log.d(TAG, "ModuleContext already exists moduleId:${existingContext.getModuleId()}")
+ val hasModuleId = existingContext.getModuleId() != null
+ return ChimeraModuleManager(context, existingContext, hasModuleId)
+ }
+
+ val loadedModuleContext: ModuleContext? = null
+
+ return ChimeraModuleManager(context, loadedModuleContext, false)
+ }
+
+ override fun createBasicModuleInfo(context: Context): BasicModuleInfo? {
+ val moduleContext = ModuleContext.getModuleContext(context) ?: return null
+ val moduleId = moduleContext.getModuleId() ?: return null
+ return BasicModuleInfo(moduleId, moduleContext.getModuleVersion(), moduleContext.getSubmoduleId())
+ }
+
+ override fun createSubmoduleContext(
+ context: Context,
+ moduleName: String,
+ require: Boolean
+ ): Context? {
+ val moduleContext = ModuleContext.getModuleContext(context)
+ if (moduleContext == null) {
+ return unavailableSubmodule(moduleName, require)
+ }
+ val moduleId = moduleContext.getModuleId()
+ if (moduleId == null) {
+ return unavailableSubmodule(moduleName, require)
+ }
+ if (!DynamicModuleSettings.isAvailable(context)) {
+ return unavailableSubmodule(moduleName, require)
+ }
+
+ val parent = ChimeraConfigManager.findModuleByModuleId(moduleId)
+ ?: return unavailableSubmodule(moduleName, require)
+ val capabilities = ChimeraApkManifestReader.readVerifiedCapabilities(context, parent)
+ val targetModuleId = resolveSubmoduleId(moduleName, capabilities)
+ ?: return unavailableSubmodule(moduleName, require)
+ val targetCapability = capabilities.first { it.moduleId == targetModuleId }
+ val target = ChimeraConfigManager.findModuleByModuleId(targetModuleId)
+ ?: return unavailableSubmodule(moduleName, require)
+
+ if (target.installedApkPath != parent.installedApkPath || target.apkSha256 != parent.apkSha256) {
+ Log.w(TAG, "Submodule $targetModuleId is not owned by the current verified APK")
+ return unavailableSubmodule(moduleName, require)
+ }
+
+ Log.d(TAG, "createSubmoduleContext($moduleName) -> $targetModuleId for moduleId=$moduleId")
+ return ChimeraModuleLdr.loadModule(
+ context,
+ targetModuleId,
+ target.moduleName,
+ targetCapability.moduleVersion,
+ ) ?: unavailableSubmodule(moduleName, require)
+ }
+
+ private fun resolveSubmoduleId(
+ requestedName: String,
+ capabilities: List,
+ ): String? {
+ if (capabilities.any { it.moduleId == requestedName }) return requestedName
+ val registered = DynamicModuleRegistry.getByModuleId(requestedName)
+ ?: DynamicModuleRegistry.modules.firstOrNull { it.moduleName == requestedName }
+ ?: return null
+ return registered.moduleIds.filter { candidate ->
+ capabilities.any { it.moduleId == candidate }
+ }.singleOrNull()
+ }
+
+ private fun unavailableSubmodule(moduleName: String, require: Boolean): Context? {
+ if (!require) return null
+ Log.w(TAG, "Required submodule context not available: $moduleName")
+ throw IllegalStateException("Submodule not available: $moduleName")
+ }
+
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraStorage.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraStorage.kt
new file mode 100644
index 0000000000..6794297ddc
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraStorage.kt
@@ -0,0 +1,208 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.Context
+import android.os.Build
+import java.io.File
+import java.security.MessageDigest
+import java.util.zip.ZipFile
+
+object ChimeraStorage {
+ const val CHIMERA_DIR = "chimera"
+ const val MODULE_SUBDIR = "m"
+ const val APK_PREFIX = "dl-"
+ const val APK_SUFFIX = ".apk"
+
+ private val MODULE_APK_RE = Regex("^${Regex.escape(APK_PREFIX)}(.+)_(\\d+)${Regex.escape(APK_SUFFIX)}$")
+ private val MODULE_CONTAINER_RE = Regex("[0-9a-fA-F]{8}")
+
+ data class ModuleApkFile(
+ val file: File,
+ val moduleName: String,
+ val version: String,
+ )
+
+ data class ModuleApkDestination(
+ val file: File,
+ val priority: Int,
+ )
+
+ fun moduleRoot(context: Context): File {
+ val base = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !context.isDeviceProtectedStorage) {
+ context.createDeviceProtectedStorageContext()
+ } else {
+ context
+ }
+ return File(base.getDir(CHIMERA_DIR, Context.MODE_PRIVATE), MODULE_SUBDIR)
+ }
+
+ fun apkFileName(moduleName: String, version: String): String =
+ "$APK_PREFIX${moduleName}_$version$APK_SUFFIX"
+
+ /**
+ * Reserves a unique Chimera container directory and returns the matching config priority.
+ * Creating the directory while holding the object monitor makes concurrent imports in the
+ * same process unable to select the same slot.
+ */
+ @Synchronized
+ fun allocateModuleApkFile(context: Context, moduleName: String, version: String): ModuleApkDestination {
+ val root = ensureModuleRoot(context)
+ var priority = 0
+ while (true) {
+ val container = File(root, String.format("%08x", priority + 1))
+ if (!container.exists()) {
+ check(container.mkdir()) { "Failed to reserve Chimera module directory: ${container.absolutePath}" }
+ makeDirectoryReadable(container)
+ return ModuleApkDestination(
+ File(container, apkFileName(moduleName, version)),
+ priority
+ )
+ }
+ check(priority < Int.MAX_VALUE - 1) { "No free Chimera module directory" }
+ priority++
+ }
+ }
+
+ fun ensureModuleRoot(context: Context): File {
+ val root = moduleRoot(context)
+ root.mkdirs()
+ root.parentFile?.let { makeDirectoryReadable(it) }
+ makeDirectoryReadable(root)
+ return root
+ }
+
+ /** Make the module apk and its container directory readable by other processes. */
+ fun makeModuleApkReadable(apkFile: File) {
+ apkFile.parentFile?.let { makeDirectoryReadable(it) }
+ apkFile.setReadable(true, false)
+ }
+
+ fun parseModuleApkFile(file: File): ModuleApkFile? {
+ val match = MODULE_APK_RE.matchEntire(file.name) ?: return null
+ return ModuleApkFile(file, match.groupValues[1], match.groupValues[2])
+ }
+
+ fun listDownloadedApks(context: Context, moduleName: String? = null): List =
+ listDownloadedApks(moduleRoot(context), moduleName)
+
+ fun listDownloadedApks(moduleRoot: File, moduleName: String? = null): List {
+ if (!moduleRoot.isDirectory) return emptyList()
+ return runCatching {
+ buildList {
+ moduleRoot.listFiles()?.forEach { entry ->
+ if (entry.isDirectory) {
+ entry.listFiles()?.forEach { file -> addIfModuleApk(file, moduleName) }
+ } else {
+ addIfModuleApk(entry, moduleName)
+ }
+ }
+ }.sortedWith(compareByDescending { it.version.toLongOrNull() ?: Long.MIN_VALUE }
+ .thenBy { it.file.absolutePath })
+ }.getOrDefault(emptyList())
+ }
+
+ fun findDownloadedApk(context: Context, moduleName: String): File? {
+ return findDownloadedApkInRoot(moduleRoot(context), moduleName)
+ }
+
+ fun findDownloadedApkInRoot(moduleRoot: File, moduleName: String): File? {
+ return listDownloadedApks(moduleRoot, moduleName).firstOrNull()?.file
+ }
+
+ /**
+ * Confirms that a config-owned module artifact is still the APK imported for that config entry.
+ * This intentionally requires a persisted digest: disk-scan recovery without one is not proof of
+ * artifact integrity and must not be reported as an installed module.
+ */
+ fun verifiedModuleApk(
+ context: Context,
+ file: File?,
+ expectedModuleName: String?,
+ expectedSha256: String?,
+ ): File? = verifiedModuleApkInRoot(
+ file = file,
+ moduleRoot = moduleRoot(context),
+ expectedModuleName = expectedModuleName,
+ expectedSha256 = expectedSha256,
+ )
+
+ /**
+ * File-only variant of [verifiedModuleApk] for callers that already own the Chimera root.
+ * A module artifact is executable only when it remains under that root and still matches the
+ * digest persisted by its import transaction.
+ */
+ internal fun verifiedModuleApkInRoot(
+ file: File?,
+ moduleRoot: File,
+ expectedModuleName: String?,
+ expectedSha256: String?,
+ ): File? {
+ val candidate = file?.canonicalOrNull() ?: return null
+ val root = moduleRoot.canonicalOrNull() ?: return null
+ if (!candidate.isUnder(root) || !candidate.isFile || !candidate.canRead()) return null
+ val parsed = parseModuleApkFile(candidate) ?: return null
+ if (!expectedModuleName.isNullOrEmpty() && parsed.moduleName != expectedModuleName) return null
+ if (expectedSha256.isNullOrEmpty() || !isApk(candidate)) return null
+ val actual = sha256Hex(candidate)
+ return candidate.takeIf { actual.equals(expectedSha256, ignoreCase = true) }
+ }
+
+ /** Deletes only a canonical dl-*.apk below this user's Chimera module root. */
+ fun safeDeleteModuleApk(context: Context, apkFile: File): Boolean = runCatching {
+ val candidate = apkFile.canonicalOrNull() ?: return@runCatching false
+ val root = moduleRoot(context).canonicalOrNull() ?: return@runCatching false
+ if (!candidate.isUnder(root) || parseModuleApkFile(candidate) == null) return@runCatching false
+ val parent = candidate.parentFile
+ val deleted = !candidate.exists() || candidate.delete()
+ cleanupEmptyModuleContainer(parent)
+ deleted
+ }.getOrDefault(false)
+
+ private fun MutableList.addIfModuleApk(file: File, moduleName: String?) {
+ if (!file.isFile) return
+ val parsed = parseModuleApkFile(file) ?: return
+ if (moduleName == null || parsed.moduleName == moduleName) add(parsed)
+ }
+
+ private fun isApk(file: File): Boolean = runCatching {
+ ZipFile(file).use { zip -> zip.getEntry("AndroidManifest.xml") != null }
+ }.getOrDefault(false)
+
+ private fun sha256Hex(file: File): String {
+ val digest = MessageDigest.getInstance("SHA-256")
+ file.inputStream().buffered().use { input ->
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ while (true) {
+ val count = input.read(buffer)
+ if (count < 0) break
+ digest.update(buffer, 0, count)
+ }
+ }
+ return digest.digest().joinToString("") { "%02x".format(it) }
+ }
+
+ private fun makeDirectoryReadable(dir: File) {
+ dir.mkdirs()
+ dir.setReadable(true, false)
+ dir.setExecutable(true, false)
+ }
+
+ private fun cleanupEmptyModuleContainer(dir: File?) {
+ dir?.takeIf {
+ it.parentFile?.name == MODULE_SUBDIR &&
+ MODULE_CONTAINER_RE.matches(it.name) &&
+ it.isDirectory &&
+ it.list()?.isEmpty() == true
+ }?.delete()
+ }
+
+ private fun File.canonicalOrNull(): File? = runCatching { canonicalFile }.getOrNull()
+
+ private fun File.isUnder(root: File): Boolean {
+ val rootPath = root.absolutePath
+ return absolutePath == rootPath || absolutePath.startsWith(rootPath + File.separator)
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/DynamicModuleSettings.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/DynamicModuleSettings.kt
new file mode 100644
index 0000000000..22aa7fcf53
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/DynamicModuleSettings.kt
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import org.microg.gms.common.Constants
+import org.microg.gms.settings.SettingsContract
+
+/** One reusable gate for every dynamic-module entry point. */
+object DynamicModuleSettings {
+ /**
+ * Dynamic module resources rely on the platform ResourcesLoader API introduced in Android 11.
+ * Keep the application itself compatible with older releases while making this optional runtime
+ * capability explicitly unavailable there.
+ */
+ @JvmStatic
+ fun isRuntimeSupported(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
+
+ @JvmStatic
+ fun isEnabled(context: Context): Boolean = runCatching {
+ val settingsContext = resolveSettingsContext(context)
+ val projection = arrayOf(SettingsContract.DynamicModule.DYNAMIC_MODULE_ENABLED)
+ SettingsContract.getSettings(
+ settingsContext,
+ SettingsContract.DynamicModule.getContentUri(settingsContext),
+ projection
+ ) { cursor -> cursor.getInt(0) != 0 }
+ }.onFailure { Log.w(TAG, "Unable to read dynamic-module setting", it) }
+ .getOrDefault(false)
+
+ /** True only when the user has enabled the feature on a platform that can load module resources. */
+ @JvmStatic
+ fun isAvailable(context: Context): Boolean = isRuntimeSupported() && isEnabled(context)
+
+ @JvmStatic
+ fun setEnabled(context: Context, enabled: Boolean): Boolean = runCatching {
+ val settingsContext = resolveSettingsContext(context)
+ SettingsContract.setSettings(
+ settingsContext,
+ SettingsContract.DynamicModule.getContentUri(settingsContext)
+ ) {
+ put(SettingsContract.DynamicModule.DYNAMIC_MODULE_ENABLED, enabled)
+ }
+ true
+ }.onFailure { Log.w(TAG, "Unable to update dynamic-module setting", it) }
+ .getOrDefault(false)
+
+ /** Dynamite code may run in a third-party process, whose package has no microG SettingsProvider. */
+ private fun resolveSettingsContext(context: Context): Context {
+ return if (context.packageName == Constants.GMS_PACKAGE_NAME) {
+ context
+ } else {
+ context.createPackageContext(Constants.GMS_PACKAGE_NAME, 0)
+ }
+ }
+
+ private const val TAG = "DynamicModuleSettings"
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/FeatureCheckUtils.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/FeatureCheckUtils.kt
new file mode 100644
index 0000000000..19b1aca3d9
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/FeatureCheckUtils.kt
@@ -0,0 +1,163 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.util.Log
+import com.google.android.chimera.config.registry.FeatureConfigRegistry
+import java.io.IOException
+
+object FeatureCheckUtils {
+ private const val TAG = "FeatureCheckUtils"
+
+ fun checkFeatureDescriptors(
+ descriptors: Iterable,
+ allowStaticRegistry: Boolean = true,
+ allowDynamicModules: Boolean = true,
+ ): Int {
+ for (fd in descriptors) {
+ val featureName = fd.featureName
+ val requestedVersion = fd.featureVersion ?: 0L
+ Log.d(TAG, "Checking feature: $featureName, requestedVersion: $requestedVersion")
+
+ val result = checkFeature(
+ featureName,
+ requestedVersion,
+ allowStaticRegistry,
+ allowDynamicModules
+ )
+ if (result != ModuleManager.FEATURE_CHECK_SUCCESS) return result
+ }
+
+ Log.d(TAG, "All features checked successfully")
+ return ModuleManager.FEATURE_CHECK_SUCCESS
+ }
+
+ fun checkFeatureListProto(bytes: ByteArray, allowDynamicModules: Boolean = true): Int {
+ val features = try {
+ FeaturesMessage.ADAPTER.decode(bytes)
+ } catch (e: IOException) {
+ Log.d(TAG, "Failed to parse FeatureList proto: ${e.message}")
+ return ModuleManager.FEATURE_CHECK_ERROR
+ }
+
+ return checkFeatureMessages(
+ features.features,
+ allowStaticRegistry = true,
+ allowDynamicModules = allowDynamicModules
+ )
+ }
+
+ fun checkFeatureMessages(
+ messages: Iterable,
+ allowStaticRegistry: Boolean = true,
+ allowDynamicModules: Boolean = true,
+ ): Int {
+ for (message in messages) {
+ val result = checkFeature(
+ message.featureName,
+ message.featureVersion ?: 0L,
+ allowStaticRegistry,
+ allowDynamicModules
+ )
+ if (result != ModuleManager.FEATURE_CHECK_SUCCESS) return result
+
+ val nested = message.featureDescriptor
+ if (nested.isNotEmpty()) {
+ val nestedResult = checkFeatureDescriptors(
+ nested,
+ allowStaticRegistry,
+ allowDynamicModules
+ )
+ if (nestedResult != ModuleManager.FEATURE_CHECK_SUCCESS) return nestedResult
+ }
+ }
+ return ModuleManager.FEATURE_CHECK_SUCCESS
+ }
+
+ private fun checkFeature(
+ featureName: String?,
+ requestedVersion: Long,
+ allowStaticRegistry: Boolean,
+ allowDynamicModules: Boolean,
+ ): Int {
+ if (featureName.isNullOrEmpty()) {
+ Log.w(TAG, "Unknown feature: $featureName")
+ return ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE
+ }
+ if (requestedVersion < -1L) {
+ Log.w(TAG, "Invalid requested version for $featureName: $requestedVersion")
+ return ModuleManager.FEATURE_CHECK_ERROR
+ }
+ // The static catalog only knows built-in aliases. An imported future Chimera module can
+ // contribute a feature descriptor without appearing there, but it must still receive the
+ // compatible "module required" result while dynamic modules are disabled.
+ if (!allowDynamicModules && (
+ ModuleDownloadRegistry.isKnownDynamicFeature(featureName) ||
+ ChimeraConfigManager.featureConfigByKey(featureName) != null
+ )
+ ) {
+ Log.d(TAG, "Dynamic feature '$featureName' is disabled")
+ return ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED
+ }
+
+ if (allowDynamicModules) {
+ ChimeraConfigManager.featureConfigByKey(featureName)?.let { installed ->
+ return evaluateKnownFeature(
+ featureName = featureName,
+ availableVersion = installed.featureVersion,
+ requestedVersion = requestedVersion,
+ source = "installed config"
+ )
+ }
+ }
+
+ val registry = FeatureConfigRegistry.featureMap[featureName]
+ if (registry == null) {
+ if (ModuleDownloadRegistry.isKnownDynamicFeature(featureName)) {
+ Log.d(TAG, "Known dynamic feature '$featureName' is not installed")
+ return ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED
+ }
+ Log.w(TAG, "Unknown feature: $featureName")
+ return ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE
+ }
+
+ if (!allowStaticRegistry) {
+ Log.d(TAG, "Feature '$featureName' is known but not installed")
+ return ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED
+ }
+
+ return evaluateKnownFeature(
+ featureName = featureName,
+ availableVersion = registry.featureVersion.toLong(),
+ requestedVersion = requestedVersion,
+ source = "static registry"
+ )
+ }
+
+ private fun evaluateKnownFeature(
+ featureName: String,
+ availableVersion: Long?,
+ requestedVersion: Long,
+ source: String
+ ): Int {
+ if (requestedVersion == 0L) {
+ Log.d(TAG, "Feature '$featureName' available from $source at any version")
+ return ModuleManager.FEATURE_CHECK_SUCCESS
+ }
+
+ if (availableVersion == null || availableVersion < 0L) {
+ Log.w(TAG, "Feature '$featureName' has no usable version in $source")
+ return ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED
+ }
+
+ if (requestedVersion == -1L || availableVersion >= requestedVersion) {
+ Log.d(TAG, "Feature '$featureName' available from $source at version $availableVersion")
+ return ModuleManager.FEATURE_CHECK_SUCCESS
+ }
+
+ Log.d(TAG, "Feature '$featureName' version $availableVersion is below requested $requestedVersion")
+ return ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/InvalidConfigException.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/InvalidConfigException.kt
new file mode 100644
index 0000000000..39dc408243
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/InvalidConfigException.kt
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import com.google.android.chimera.annotation.ChimeraApiVersion
+
+@ChimeraApiVersion(added = 0L)
+class InvalidConfigException : Exception {
+ constructor(s: String?) : super(s)
+
+ constructor(s: String?, throwable0: Throwable?) : super(s, throwable0)
+
+ constructor(throwable0: Throwable?) : super(throwable0)
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleDownloadRegistry.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleDownloadRegistry.kt
new file mode 100644
index 0000000000..a4c707a4cc
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleDownloadRegistry.kt
@@ -0,0 +1,383 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.Manifest
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.util.Log
+import androidx.annotation.StringRes
+import com.google.android.chimera.component.ModuleDownloadActivity
+import com.google.android.chimera.component.TAG
+import com.google.android.chimera.config.registry.DynamicModuleRegistry
+import com.google.android.gms.common.Feature
+import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer
+import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest
+import org.microg.gms.chimera.core.R
+import java.io.File
+import java.util.UUID
+
+/** Resolves requested Chimera features to independently downloadable `.mods` release assets. */
+object ModuleDownloadRegistry {
+ /** [ApiFeatureRequest] retained by the internal module authorization activity. */
+ const val EXTRA_API_FEATURE_REQUEST =
+ "com.google.android.chimera.config.EXTRA_API_FEATURE_REQUEST"
+ private const val FEATURE_MLKIT_DOCUMENT_SCANNER = "mlkit.docscan.ui"
+ private const val COMPONENT_MLKIT_DOCUMENT_SCANNER =
+ "com.google.android.gms.mlkit.docscan.ui.DocumentScanningActivity"
+ private const val MODULE_RELEASE_BASE_URL =
+ "https://github.com/david200101/gms-chimera/releases/download/v1.0.0/"
+ private const val DOCUMENT_SCANNER_BUNDLE = "mlkit-document-scanner.mods"
+ private const val DOCUMENT_SCANNER_X86_BUNDLE = "mlkit-document-scanner_x86.mods"
+
+ data class RequestedFeature(
+ val name: String,
+ val minVersion: Long = 0L,
+ )
+
+ data class PermissionRequirement(
+ val permission: String,
+ @StringRes val labelRes: Int,
+ @StringRes val descriptionRes: Int
+ )
+
+ data class DownloadableModule(
+ val catalogId: String,
+ @StringRes val displayNameRes: Int,
+ val downloadUrl: String,
+ val requestedFeatures: Set,
+ val requiredPermissions: List,
+ /** Every signed Chimera module ID that must be installed before the feature may be resumed. */
+ val requiredModuleIds: Set = emptySet(),
+ /** Additional component routes required by this module, independent of the component that triggered it. */
+ val requiredComponentClassNames: Set = emptySet(),
+ )
+
+ private val modules = listOf(
+ DownloadableModule(
+ catalogId = "mlkit-document-scanner",
+ displayNameRes = R.string.chimera_module_document_scanner_name,
+ downloadUrl = documentScannerDownloadUrl(),
+ requestedFeatures = setOf(FEATURE_MLKIT_DOCUMENT_SCANNER),
+ requiredModuleIds = DynamicModuleRegistry.MLKIT_DOCUMENT_SCANNER_MODULE_IDS,
+ requiredComponentClassNames = setOf(COMPONENT_MLKIT_DOCUMENT_SCANNER),
+ requiredPermissions = listOf(
+ PermissionRequirement(
+ permission = Manifest.permission.CAMERA,
+ labelRes = R.string.chimera_permission_camera_label,
+ descriptionRes = R.string.chimera_permission_camera_description
+ )
+ )
+ )
+ )
+
+ private fun documentScannerDownloadUrl(): String {
+ val supportedAbis = if (Build.VERSION.SDK_INT >= 21) {
+ Build.SUPPORTED_ABIS.asIterable()
+ } else {
+ listOfNotNull(Build.CPU_ABI, Build.CPU_ABI2.takeIf(String::isNotEmpty))
+ }
+ val bundle = if (supportedAbis.any { it == "x86" || it == "x86_64" }) {
+ DOCUMENT_SCANNER_X86_BUNDLE
+ } else {
+ DOCUMENT_SCANNER_BUNDLE
+ }
+ return MODULE_RELEASE_BASE_URL + bundle
+ }
+
+ /** Returns every independently downloadable module needed by [requestedFeatures], in catalog order. */
+ @JvmStatic
+ fun resolveModules(requestedFeatures: Iterable): List {
+ val requestedNames = normalizeRequestedFeatures(requestedFeatures).mapTo(mutableSetOf()) { it.name }
+ return modules.filter { candidate ->
+ candidate.requestedFeatures.any(requestedNames::contains)
+ }
+ }
+
+ /** True for a feature that is provided by a known, independently downloadable module. */
+ @JvmStatic
+ fun isKnownDynamicFeature(featureName: String?): Boolean {
+ if (featureName.isNullOrEmpty()) return false
+ return modules.any { featureName in it.requestedFeatures }
+ }
+
+ /** Creates the internal permission-and-download page for a known module request. */
+ @JvmStatic
+ fun createModuleDownloadIntent(context: Context, requestedFeatureNames: Iterable): Intent? {
+ return createModuleDownloadIntentForRequests(
+ context,
+ requestedFeatureNames.map(::RequestedFeature),
+ )
+ }
+
+ /** Creates the internal permission-and-download page while retaining requested minimum versions. */
+ @JvmStatic
+ fun createModuleDownloadIntentForRequests(
+ context: Context,
+ requestedFeatures: Iterable,
+ ): Intent? {
+ val features = normalizeRequestedFeatures(requestedFeatures)
+ val availabilityRequest = ApiFeatureRequest().apply {
+ this.features = features.map { Feature(it.name, it.minVersion) }
+ }
+ return createModuleDownloadIntentForRequests(context, features, availabilityRequest)
+ }
+
+ /**
+ * Creates the internal permission-and-download page while retaining the original API request
+ * for the external download application's post-download availability check.
+ */
+ @JvmStatic
+ fun createModuleDownloadIntentForRequests(
+ context: Context,
+ requestedFeatures: Iterable,
+ apiFeatureRequest: ApiFeatureRequest?,
+ ): Intent? {
+ if (!DynamicModuleSettings.isAvailable(context)) return null
+ val features = normalizeRequestedFeatures(requestedFeatures)
+ if (features.isEmpty() ||
+ features.any { it.minVersion < -1L || !isKnownDynamicFeature(it.name) }
+ ) return null
+ return Intent(context, ModuleDownloadActivity::class.java).apply {
+ putRequestedFeatures(features)
+ apiFeatureRequest?.let { putExtra(EXTRA_API_FEATURE_REQUEST, SafeParcelableSerializer.serializeToBytes(it)) }
+ putRequestIdentity("download")
+ putContainerActivity(context)
+ }
+ }
+
+ @JvmStatic
+ fun createModuleDownloadIntent(context: Context, requestedFeatureNames: String?): Intent? {
+ return createModuleDownloadIntent(context, splitFeatureNames(requestedFeatureNames))
+ }
+
+ /** Creates the permission page used when an installed module was launched after permission revocation. */
+ @JvmStatic
+ fun createModulePermissionIntent(
+ context: Context,
+ requestedFeatureNames: String?
+ ): Intent? {
+ if (!DynamicModuleSettings.isAvailable(context)) return null
+ val features = splitFeatureNames(requestedFeatureNames)
+ .map(::RequestedFeature)
+ if (features.isEmpty() || features.any { !isKnownDynamicFeature(it.name) }) return null
+ return Intent(context, ModuleDownloadActivity::class.java).apply {
+ putRequestedFeatures(features)
+ putRequestIdentity("permission")
+ putExtra(
+ ModuleDownloadActivity.EXTRA_AFTER_AUTHORIZATION_ACTION,
+ ModuleDownloadActivity.ACTION_CONTINUE_MODULE
+ )
+ putContainerActivity(context)
+ }
+ }
+
+ @JvmStatic
+ fun requestedFeatureNamesForActivity(context: Context, componentClassName: String): String? {
+ modules.firstOrNull { componentClassName in it.requiredComponentClassNames }
+ ?.requestedFeatures
+ ?.joinToString(",")
+ ?.let { return it }
+ return try {
+ context.packageManager.getActivityInfo(
+ ComponentName(context, componentClassName),
+ PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.GET_META_DATA
+ ).metaData?.getString("chimera.requested_features")
+ } catch (_: PackageManager.NameNotFoundException) {
+ null
+ }
+ }
+
+ @JvmStatic
+ fun hasMissingPermissions(context: Context, requestedFeatureNames: String?): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false
+ val resolvedModules = resolveModules(splitFeatureNames(requestedFeatureNames).map(::RequestedFeature))
+ return resolvedModules.any { module ->
+ module.requiredPermissions.any {
+ context.checkSelfPermission(it.permission) != PackageManager.PERMISSION_GRANTED
+ }
+ }
+ }
+
+ /**
+ * Re-reads the cross-process Chimera manifest and verifies the requested feature versions plus
+ * every required module artifact and component route. This is the authoritative gate used after
+ * a `.mods` import; the import completion broadcast itself is deliberately only a hint.
+ */
+ @JvmStatic
+ fun isModuleInstalled(
+ context: Context,
+ module: DownloadableModule,
+ requestedFeatures: Iterable,
+ componentClassName: String?,
+ ): Boolean {
+ return runCatching {
+ if (!DynamicModuleSettings.isAvailable(context)) return@runCatching false
+ ChimeraModuleBootstrap.ensureInitialized(context)
+ ChimeraConfigManager.reload()
+ val relevantFeatures = normalizeRequestedFeatures(requestedFeatures)
+ .filter { it.name in module.requestedFeatures }
+ if (relevantFeatures.isEmpty()) return@runCatching false
+ val featuresAvailable = relevantFeatures.all { requestedFeature ->
+ val descriptor = ChimeraConfigManager.featureConfigByKey(requestedFeature.name)
+ ?: return@all false
+ isVersionSatisfied(descriptor.featureVersion, requestedFeature.minVersion)
+ }
+ val moduleIdsAvailable = module.requiredModuleIds.all { moduleId ->
+ verifiedInstalledArtifact(context, ChimeraConfigManager.findModuleByModuleId(moduleId))
+ }
+ val requiredComponents = buildSet {
+ addAll(module.requiredComponentClassNames)
+ componentClassName?.takeIf { it.isNotEmpty() }?.let(::add)
+ }
+ val componentsAvailable = requiredComponents.all { className ->
+ verifiedInstalledArtifact(context, ChimeraConfigManager.findModuleByComponent(className))
+ }
+ featuresAvailable && moduleIdsAvailable && componentsAvailable
+ }.getOrDefault(false)
+ }
+
+ @JvmStatic
+ fun isModuleInstalled(
+ context: Context,
+ module: DownloadableModule,
+ componentClassName: String?,
+ ): Boolean = isModuleInstalled(
+ context,
+ module,
+ module.requestedFeatures.map(::RequestedFeature),
+ componentClassName,
+ )
+
+ /**
+ * Creates the external HTTPS intent without app-private parcelables. Generic system resolvers
+ * cannot load GMS classes; the request remains persisted inside GMS until import completes.
+ */
+ @JvmStatic
+ fun createExternalDownloadIntent(
+ downloadUrl: String,
+ apiFeatureRequest: ApiFeatureRequest?,
+ ): Intent {
+ require(modules.any { it.downloadUrl == downloadUrl }) { "Unknown module download URL" }
+ val uri = Uri.parse(downloadUrl)
+ require(uri.scheme == "https") { "Module download URL must use HTTPS" }
+ return Intent(Intent.ACTION_VIEW, uri).apply {
+ apiFeatureRequest?.let { putExtra(EXTRA_API_FEATURE_REQUEST, SafeParcelableSerializer.serializeToBytes(it)) }
+ Log.d(TAG, "Creating external module download intent for ${apiFeatureRequest?.features?.size} feature entries")
+ }
+ }
+
+ /** Builds a system chooser that reports an actual target selection when the platform supports it. */
+ @JvmStatic
+ fun createExternalDownloadChooserIntent(
+ downloadUrl: String,
+ title: CharSequence?,
+ apiFeatureRequest: ApiFeatureRequest?,
+ selectionCallback: IntentSender?,
+ ): Intent {
+ val downloadIntent = createExternalDownloadIntent(downloadUrl, apiFeatureRequest)
+ return if (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 &&
+ selectionCallback != null
+ ) {
+ Intent.createChooser(downloadIntent, title, selectionCallback)
+ } else {
+ Intent.createChooser(downloadIntent, title)
+ }
+ }
+
+ private fun splitFeatureNames(requestedFeatureNames: String?): List {
+ return requestedFeatureNames.orEmpty().split(',', ';')
+ }
+
+ fun requestedFeaturesFromIntent(intent: Intent): List {
+ val names = intent.getStringArrayListExtra(ModuleDownloadActivity.EXTRA_REQUESTED_FEATURE_NAMES)
+ .orEmpty()
+ val versions = intent.getLongArrayExtra(ModuleDownloadActivity.EXTRA_REQUESTED_FEATURE_VERSIONS)
+ return normalizeRequestedFeatures(names.mapIndexed { index, name ->
+ RequestedFeature(name, versions?.getOrNull(index) ?: 0L)
+ })
+ }
+
+ private fun normalizeRequestedFeatures(
+ requestedFeatures: Iterable,
+ ): List {
+ val normalized = linkedMapOf()
+ requestedFeatures.forEach { requestedFeature ->
+ val name = requestedFeature.name.trim()
+ if (name.isEmpty()) return@forEach
+ normalized[name] = normalized[name]
+ ?.let { mergeMinimumVersion(it, requestedFeature.minVersion) }
+ ?: requestedFeature.minVersion
+ }
+ return normalized.map { (name, minVersion) -> RequestedFeature(name, minVersion) }
+ }
+
+ private fun mergeMinimumVersion(first: Long, second: Long): Long = when {
+ first == 0L -> second
+ second == 0L -> first
+ first == -1L -> second
+ second == -1L -> first
+ else -> maxOf(first, second)
+ }
+
+ private fun isVersionSatisfied(availableVersion: Long?, requestedVersion: Long): Boolean {
+ if (requestedVersion == 0L) return true
+ if (availableVersion == null || availableVersion < 0L || requestedVersion < -1L) return false
+ return requestedVersion == -1L || availableVersion >= requestedVersion
+ }
+
+ private fun verifiedInstalledArtifact(context: Context, module: ChimeraModule?): Boolean {
+ val path = module?.installedApkPath?.takeIf { it.isNotEmpty() } ?: return false
+ return ChimeraStorage.verifiedModuleApk(
+ context = context,
+ file = File(path),
+ expectedModuleName = null,
+ expectedSha256 = module.apkSha256,
+ ) != null
+ }
+
+ private fun Intent.putRequestedFeatures(features: List) {
+ putStringArrayListExtra(
+ ModuleDownloadActivity.EXTRA_REQUESTED_FEATURE_NAMES,
+ ArrayList(features.map(RequestedFeature::name)),
+ )
+ putExtra(
+ ModuleDownloadActivity.EXTRA_REQUESTED_FEATURE_VERSIONS,
+ features.map(RequestedFeature::minVersion).toLongArray(),
+ )
+ }
+
+ /** Makes otherwise identical PendingIntents independent; extras are not part of PendingIntent identity. */
+ private fun Intent.putRequestIdentity(action: String) {
+ val requestId = UUID.randomUUID().toString()
+ putExtra(ModuleDownloadActivity.EXTRA_REQUEST_ID, requestId)
+ data = Uri.Builder()
+ .scheme("chimera-module")
+ .authority(action)
+ .appendPath(requestId)
+ .build()
+ }
+
+ /**
+ * Component verification is meaningful only for a Chimera Activity request. ModuleInstallService
+ * also creates this page; persisting its Service class here would make an otherwise successful
+ * import fail the Activity-route check forever.
+ */
+ private fun Intent.putContainerActivity(context: Context) {
+ if (context is Activity) {
+ putExtra(
+ ModuleDownloadActivity.EXTRA_CONTAINER_COMPONENT_CLASS_NAME,
+ context.javaClass.name
+ )
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleManager.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleManager.kt
new file mode 100644
index 0000000000..80df38323a
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleManager.kt
@@ -0,0 +1,292 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config
+
+import android.content.Context
+import android.os.Bundle
+import com.google.android.chimera.annotation.ChimeraApiVersion
+import java.util.Collections
+import java.util.Map
+
+@ChimeraApiVersion(added = 0L)
+abstract class ModuleManager {
+
+ companion object {
+ const val FEATURE_CHECK_SUCCESS = 0
+ const val FEATURE_CHECK_UNKNOWN_FEATURE = 1
+ const val FEATURE_CHECK_UPDATE_REQUIRED = 2
+ const val FEATURE_CHECK_ERROR = 3
+
+ @ChimeraApiVersion(added = 105L)
+ const val FEATURE_REQUEST_RESULT_FAILURE = 1
+
+ @ChimeraApiVersion(added = 105L)
+ const val FEATURE_REQUEST_RESULT_FAILURE_NO_RETRY = 2
+
+ @ChimeraApiVersion(added = 105L)
+ const val FEATURE_REQUEST_RESULT_SUCCESS = 0
+
+ private var supplier: ModuleManagerSupplier = ChimeraModuleManager.ChimeraModuleManagerSupplier()
+
+ @ChimeraApiVersion(added = 0x79L)
+ @JvmStatic
+ fun createSubmoduleContext(context: Context, moduleName: String): Context? {
+ return supplier.createSubmoduleContext(context, moduleName, false)
+ }
+
+ @ChimeraApiVersion(added = 0x7BL)
+ @JvmStatic
+ fun requireSubmoduleContext(context: Context, moduleName: String): Context {
+ val ctx = supplier.createSubmoduleContext(context, moduleName, true)
+ return ctx ?: throw IllegalStateException()
+ }
+
+ @JvmStatic
+ fun get(context: Context): ModuleManager {
+ return supplier.createModuleManager(context)
+ }
+
+ @JvmStatic
+ fun getBasicModuleInfo(context: Context): BasicModuleInfo? {
+ return supplier.createBasicModuleInfo(context)
+ }
+
+ @JvmStatic
+ fun setModuleManagerSupplier(moduleManagerSupplier: ModuleManagerSupplier) {
+ supplier = moduleManagerSupplier
+ }
+ }
+
+ @ChimeraApiVersion(added = 104L)
+ abstract fun checkFeaturesAreAvailable(featureCheck: FeatureCheck): Int
+
+ @Deprecated("Use checkFeaturesAreAvailable with FeatureCheck")
+ abstract fun checkFeaturesAreAvailable(featureList: FeatureList): Int
+
+ abstract fun fetchFeatures(features: Array): FeatureList?
+
+ abstract fun getAllModules(): Collection<*>
+
+ abstract fun getAllModulesWithMetadata(moduleName: String): Collection<*>
+
+ @ChimeraApiVersion(added = 100L)
+ abstract fun getApiVersion(moduleName: String): Int
+
+ abstract fun getCurrentConfig(): ConfigInfo?
+
+ abstract fun getCurrentModule(): ModuleInfo?
+
+ abstract fun getCurrentModuleApk(): ModuleApkInfo?
+
+ @Deprecated("Use other license mechanism")
+ abstract fun getThirdPartyLicenses(): Map<*, *>
+
+ @ChimeraApiVersion(added = 0x7CL)
+ abstract fun pauseModuleUpdates(moduleName: String, flags: Int)
+
+ abstract fun requestFeatures(request: FeatureRequest): Boolean
+
+ @ChimeraApiVersion(added = 0x7CL)
+ abstract fun resumeModuleUpdates(moduleName: String)
+
+ interface ModuleManagerSupplier {
+ fun createModuleManager(context: Context): ModuleManager
+ fun createBasicModuleInfo(context: Context): BasicModuleInfo?
+ fun createSubmoduleContext(context: Context, moduleName: String, require: Boolean): Context?
+ }
+
+ @ChimeraApiVersion(added = 104L)
+ class FeatureCheck {
+ private val _featureDescriptors = ArrayList()
+ val featureDescriptors: List get() = _featureDescriptors
+
+ private fun addFeature(feature: String, status: Long) {
+ _featureDescriptors.add(FeatureDescriptor(feature, status))
+ }
+
+ fun checkFeatureAtAnyVersion(feature: String): FeatureCheck {
+ addFeature(feature, 0)
+ return this
+ }
+
+ fun checkFeatureAtLatestVersion(feature: String): FeatureCheck {
+ addFeature(feature, -1)
+ return this
+ }
+
+ fun checkFeatureAtVersion(feature: String, version: Long): FeatureCheck {
+ require(version >= -1)
+ addFeature(feature, version)
+ return this
+ }
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ class FeatureList private constructor(private val protoBytes: ByteArray?) {
+
+ companion object {
+ fun fromDescriptors(featureDataList: List) = FeatureList(
+ FeaturesMessage.Builder()
+ .features(featureDataList.map { fd ->
+ FeatureMessage.Builder().featureName(fd.featureName).featureVersion(fd.featureVersion).build()
+ })
+ .build()
+ .encode()
+ )
+ }
+
+ fun getProtoBytes(): ByteArray? = protoBytes
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ open class BasicModuleInfo(
+ @JvmField
+ val moduleId: String?,
+ @JvmField
+ val moduleVersion: Int,
+ @JvmField
+ @ChimeraApiVersion(added = 123L) val submoduleId: String? = null
+ ) {
+
+ @ChimeraApiVersion(added = 123L)
+ fun requireSubmoduleId(): String {
+ require(submoduleId != null) { "The context used to obtain the module info for the $moduleId module is not associated with a submoduleId." }
+ return submoduleId
+ }
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ abstract class ModuleInfo(
+ moduleId: String?,
+ moduleVersion: Int,
+ submoduleId: String? = null,
+ @ChimeraApiVersion(added = 138L) val configurationMode: String?,
+ val moduleApk: ModuleApkInfo?
+ ) : BasicModuleInfo(moduleId, moduleVersion, submoduleId) {
+ abstract fun getMetadata(): Bundle
+
+ @ChimeraApiVersion(added = 0L)
+ open fun getMetadata(context: Context): Bundle = getMetadata()
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ class FeatureRequest {
+ private val _requestedFeatures = mutableMapOf()
+ private val _unrequestedFeatures = mutableSetOf()
+ private var _forceUnrequest = false
+ private var _urgent = false
+ private var _listener: FeatureRequestProgressListener? = null
+ private var _sessionId: String? = null
+ private var _requesterAppPackage: String? = null
+
+ fun getRequestedFeatures(): kotlin.collections.Map = _requestedFeatures.toMap()
+
+ fun getUnrequestedFeatures(): Set = _unrequestedFeatures.toSet()
+
+ fun getForceUnrequest(): Boolean = _forceUnrequest
+
+ fun getUrgent(): Boolean = _urgent
+
+ fun getListener(): FeatureRequestProgressListener? = _listener
+
+ fun getSessionId(): String? = _sessionId
+
+ fun getRequesterAppPackage(): String? = _requesterAppPackage
+
+ fun requestFeatureAtAnyVersion(feature: String): FeatureRequest {
+ _requestedFeatures[feature] = 0L
+ return this
+ }
+
+ fun requestFeatureAtLatestVersion(feature: String): FeatureRequest {
+ _requestedFeatures[feature] = -1L
+ return this
+ }
+
+ fun requestFeatureAtVersion(feature: String, version: Long): FeatureRequest {
+ require(version >= 0 || version == -1L) { "Invalid version: $version" }
+ _requestedFeatures[feature] = version
+ return this
+ }
+
+ fun unrequestFeature(feature: String): FeatureRequest {
+ _unrequestedFeatures.add(feature)
+ return this
+ }
+
+ fun setForceUnrequest(): FeatureRequest {
+ _forceUnrequest = true
+ return this
+ }
+
+ @ChimeraApiVersion(added = 0x6FL)
+ fun setRequesterAppPackage(pkg: String): FeatureRequest {
+ _requesterAppPackage = pkg
+ return this
+ }
+
+ @ChimeraApiVersion(added = 106L)
+ fun setSessionId(sessionId: String): FeatureRequest {
+ _sessionId = sessionId
+ return this
+ }
+
+ fun setUrgent(): FeatureRequest {
+ _urgent = true
+ return this
+ }
+
+ fun setUrgent(listener: FeatureRequestProgressListener): FeatureRequest {
+ _urgent = true
+ _listener = listener
+ return this
+ }
+
+ override fun toString(): String {
+ return "FeatureRequest{" +
+ "requestedFeatures=$_requestedFeatures, " +
+ "unrequestedFeatures=$_unrequestedFeatures, " +
+ "forceUnrequest=$_forceUnrequest, " +
+ "isUrgent=$_urgent, " +
+ "listener=$_listener, " +
+ "sessionId=$_sessionId, " +
+ "requesterAppPackage=$_requesterAppPackage" +
+ "}"
+ }
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ abstract class FeatureRequestProgressListener {
+ @Deprecated("Use onRequestComplete(Int)")
+ open fun onRequestComplete() {
+ }
+
+ @ChimeraApiVersion(added = 105L)
+ open fun onRequestComplete(result: Int) {
+ @Suppress("DEPRECATION")
+ onRequestComplete()
+ }
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ class ConfigInfo(
+ moduleSets: List<*>,
+ optionalModules: List<*>,
+ val chimeraConfigModifierFlags: Int
+ ) {
+ val moduleSets: List<*> = Collections.unmodifiableList(moduleSets)
+ val optionalModules: List<*> = Collections.unmodifiableList(optionalModules)
+ }
+
+ @ChimeraApiVersion(added = 0L)
+ data class ModuleApkInfo(
+ val apkPackageName: String,
+ val apkVersionName: String,
+ val apkVersionCode: Int,
+ val apkType: Int,
+ val apkTimestamp: Long,
+ val apkRequired: Boolean
+ )
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkRegistry.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkRegistry.kt
new file mode 100644
index 0000000000..792c3796e7
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkRegistry.kt
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config.registry
+
+object ApkRegistry {
+ class ApkInfo(
+ val apkType: ApkType,
+ val apkPath: String,
+ val moduleApiClassName: String,
+ val packageName: String,
+ val versionCode: String,
+ val dependCount: Int,
+ val dependModuleNames: Array,
+ val moduleType: Int,
+ val sourceUri: String,
+ val moduleName: String,
+ val moduleVersion: String,
+ val apkSha256: String,
+ )
+
+ fun createDynamicApkInfo(
+ apkPath: String,
+ moduleName: String,
+ moduleVersion: String,
+ moduleApiClassName: String,
+ sourceUri: String = "",
+ apkSha256: String = ""
+ ): ApkInfo {
+ return ApkInfo(
+ ApkType.FILE, apkPath, moduleApiClassName,
+ "com.google.android.gms", "0",
+ 1, arrayOf("ROOT"),
+ 0, sourceUri.ifEmpty { "file://$apkPath" }, moduleName, moduleVersion, apkSha256
+ )
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkType.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkType.kt
new file mode 100644
index 0000000000..10eb6ebce7
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkType.kt
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config.registry
+
+enum class ApkType {
+ CONTAINER,
+ FILE
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ContainerRouteRegistry.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ContainerRouteRegistry.kt
new file mode 100644
index 0000000000..f49da599b7
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ContainerRouteRegistry.kt
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config.registry
+
+import com.google.android.chimera.config.ComponentRoute
+
+object ContainerRouteRegistry {
+
+ val serviceRoutes: List = listOf(
+ ComponentRoute.build {
+ containerName = ".chimera.container.moduleinstall.ModuleInstallService.START"
+ moduleChimeraName = ".chimera.GmsApiService"
+ },
+ ComponentRoute.build {
+ containerName = ".phenotype.service.START"
+ moduleChimeraName = ".chimera.PersistentApiService"
+ },
+ ComponentRoute.build {
+ containerName = ".clearcut.service.START"
+ moduleChimeraName = ".chimera.PersistentDirectBootAwareApiService"
+ },
+ )
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/DynamicModuleRegistry.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/DynamicModuleRegistry.kt
new file mode 100644
index 0000000000..d2f2083c70
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/DynamicModuleRegistry.kt
@@ -0,0 +1,79 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config.registry
+
+object DynamicModuleRegistry {
+
+ const val MODULE_ID_MLKIT_DOCSCAN_CROP = "com.google.android.gms.mlkit_docscan_crop"
+ const val MODULE_ID_MLKIT_DOCSCAN_DETECT = "com.google.android.gms.mlkit_docscan_detect"
+ const val MODULE_ID_MLKIT_DOCSCAN_ENHANCE = "com.google.android.gms.mlkit_docscan_enhance"
+ const val MODULE_ID_MLKIT_DOCSCAN_UI = "com.google.android.gms.mlkit_docscan_ui"
+
+ val MLKIT_DOCUMENT_SCANNER_MODULE_IDS: Set = linkedSetOf(
+ MODULE_ID_MLKIT_DOCSCAN_CROP,
+ MODULE_ID_MLKIT_DOCSCAN_DETECT,
+ MODULE_ID_MLKIT_DOCSCAN_ENHANCE,
+ MODULE_ID_MLKIT_DOCSCAN_UI,
+ )
+
+ data class DynamicModule(
+ val moduleName: String,
+ val moduleIds: List = emptyList(),
+ ) {
+ val primaryModuleId: String get() = moduleIds.firstOrNull().orEmpty()
+ }
+
+ val modules: List = listOf(
+ DynamicModule(
+ moduleName = "ROOT",
+ moduleIds = listOf("", "com.google.android.gms.tflite"),
+ ),
+ DynamicModule(
+ moduleName = "MlkitDocscan.optional",
+ moduleIds = listOf(
+ MODULE_ID_MLKIT_DOCSCAN_CROP,
+ MODULE_ID_MLKIT_DOCSCAN_DETECT,
+ MODULE_ID_MLKIT_DOCSCAN_ENHANCE,
+ ),
+ ),
+ DynamicModule(
+ moduleName = "MlkitDocscanUi.optional",
+ moduleIds = listOf(MODULE_ID_MLKIT_DOCSCAN_UI),
+ ),
+ DynamicModule(
+ moduleName = "TfliteDynamiteDynamite.integ",
+ moduleIds = listOf("com.google.android.gms.tflite_dynamite"),
+ ),
+ )
+
+ private const val GMS_MODULE_PREFIX = "com.google.android.gms."
+
+ private val byModuleId: Map =
+ modules.flatMap { m -> m.moduleIds.map { it to m } }.toMap()
+
+ fun getByModuleId(moduleId: String): DynamicModule? {
+ byModuleId[moduleId]?.let { return it }
+ for (alias in moduleIdAliases(moduleId)) {
+ byModuleId[alias]?.let { return it }
+ }
+ return null
+ }
+
+ /**
+ * Stable, trusted storage/config name for a signed module ID. Unknown future modules use their signed
+ * moduleId directly; an unsigned .mods path or mapping name is never used as an ownership key.
+ */
+ fun canonicalModuleName(moduleId: String): String = getByModuleId(moduleId)?.moduleName ?: moduleId
+
+ private fun moduleIdAliases(moduleId: String): List {
+ if (!moduleId.startsWith(GMS_MODULE_PREFIX)) return emptyList()
+ val suffix = moduleId.removePrefix(GMS_MODULE_PREFIX)
+ val aliases = linkedSetOf()
+ aliases += GMS_MODULE_PREFIX + suffix.replace('_', '.')
+ aliases += GMS_MODULE_PREFIX + suffix.replace('.', '_')
+ aliases.remove(moduleId)
+ return aliases.toList()
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/FeatureConfigRegistry.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/FeatureConfigRegistry.kt
new file mode 100644
index 0000000000..44e465a7ee
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/FeatureConfigRegistry.kt
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.config.registry
+
+data class FeatureConfig(
+ val featureName: String,
+ val featureVersion: Int
+)
+
+object FeatureConfigRegistry {
+ private val features = listOf(
+ FeatureConfig("chimera_debug", 1),
+ FeatureConfig("dynamiteloader", 2),
+ FeatureConfig("loader_mp_result_code", 1),
+ FeatureConfig("mlkit.barcode.ui", 1),
+ FeatureConfig("module_flag_control", 1),
+ FeatureConfig("moduleinstall", 7),
+ FeatureConfig("vision.barcode", 1),
+ )
+ val featureMap: Map = features.associateBy { it.featureName }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/container/ModuleApi.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/container/ModuleApi.kt
new file mode 100644
index 0000000000..6071a08efe
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/container/ModuleApi.kt
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.container
+
+import android.content.Context
+import androidx.annotation.Keep
+import com.google.android.chimera.loader.ChimeraModuleApk
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Method
+
+@Keep
+abstract class ModuleApi {
+
+ protected companion object {
+ @JvmStatic
+ @Throws(Exception::class)
+ protected fun invokeStaticMethod(method: Method, args: Array) {
+ try {
+ method.invoke(null, *args)
+ } catch (invocationTargetException: InvocationTargetException) {
+ val cause = invocationTargetException.cause
+ throw if (cause is Exception) cause else Exception(cause)
+ } catch (throwable: VerifyError) {
+ throw Exception(throwable)
+ } catch (throwable: ExceptionInInitializerError) {
+ throw Exception(throwable)
+ }
+ }
+ }
+
+ abstract fun onApkLoaded(context: Context)
+
+ open fun onBeforeApkLoad(context: Context, moduleApk: ChimeraModuleApk) {
+ }
+
+ open fun onModuleLoaded(moduleName: String, providerClass: String?, context: Context) {
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ContextThemeWrapper.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ContextThemeWrapper.kt
new file mode 100644
index 0000000000..fd4431ae04
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ContextThemeWrapper.kt
@@ -0,0 +1,86 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.context
+
+import android.R
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.res.AssetManager
+import android.content.res.Resources
+import android.view.LayoutInflater
+import com.google.android.chimera.annotation.ChimeraApiVersion
+
+@ChimeraApiVersion(added = 0)
+open class ContextThemeWrapper : ContextWrapper {
+ private var themeResId: Int = 0
+ private var currentTheme: Resources.Theme? = null
+ private var cachedInflater: LayoutInflater? = null
+
+ protected constructor(base: Context?) : super(base)
+
+ constructor(base: Context?, themeResId: Int) : super(base) {
+ this.themeResId = themeResId
+ }
+
+ constructor(base: Context?, theme: Resources.Theme?) : super(base) {
+ this.currentTheme = theme
+ }
+
+ private fun ensureTheme() {
+ val firstInit = currentTheme == null
+ if (firstInit) {
+ currentTheme = resources.newTheme()
+ baseContext.theme?.let { baseTheme ->
+ currentTheme?.setTo(baseTheme)
+ }
+ }
+ onApplyThemeResource(currentTheme!!, themeResId, firstInit)
+ }
+
+ override fun getAssets(): AssetManager {
+ return resources.assets
+ }
+
+ override fun getSystemService(name: String): Any? {
+ return if (LAYOUT_INFLATER_SERVICE != name) {
+ super.getSystemService(name)
+ } else {
+ if (cachedInflater == null) {
+ cachedInflater = LayoutInflater.from(baseContext).cloneInContext(this)
+ }
+ cachedInflater
+ }
+ }
+
+ override fun getTheme(): Resources.Theme {
+ if (currentTheme != null) {
+ return currentTheme!!
+ }
+ var resolvedThemeResId = themeResId
+ val targetSdk = applicationInfo.targetSdkVersion
+ if (resolvedThemeResId == 0) {
+ resolvedThemeResId = when {
+ targetSdk < 11 -> R.style.Theme
+ targetSdk < 14 -> R.style.Theme_Holo
+ targetSdk < 24 -> R.style.Theme_DeviceDefault
+ else -> R.style.Theme_DeviceDefault_Light_DarkActionBar
+ }
+ }
+ themeResId = resolvedThemeResId
+ ensureTheme()
+ return currentTheme!!
+ }
+
+ protected open fun onApplyThemeResource(theme: Resources.Theme, resId: Int, first: Boolean) {
+ theme.applyStyle(resId, true)
+ }
+
+ override fun setTheme(resId: Int) {
+ if (themeResId != resId) {
+ themeResId = resId
+ ensureTheme()
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/context/GmsContextWrapper.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/GmsContextWrapper.kt
new file mode 100644
index 0000000000..e9924aff40
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/GmsContextWrapper.kt
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.context
+
+import android.app.BroadcastOptions
+import android.content.ComponentCallbacks
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import org.microg.gms.profile.Build.VERSION.SDK_INT
+
+class GmsContextWrapper(
+ private val wrappedContext: Context,
+ skipInternalOrImplicitChecks: Boolean = false
+) : ContextWrapper(wrappedContext) {
+
+ companion object {
+ private fun getDefaultBundle(bundle: Bundle?): Bundle? {
+ return if (bundle == null && SDK_INT >= 34) {
+ BroadcastOptions.makeBasic().apply {
+ setShareIdentityEnabled(true)
+ }.toBundle()
+ } else {
+ bundle
+ }
+ }
+ }
+
+ override fun getApplicationContext(): Context = GmsContextWrapper(wrappedContext.applicationContext)
+
+ override fun registerComponentCallbacks(callback: ComponentCallbacks?) {
+ super.getApplicationContext().registerComponentCallbacks(callback)
+ }
+
+ override fun unregisterComponentCallbacks(callback: ComponentCallbacks?) {
+ super.getApplicationContext().unregisterComponentCallbacks(callback)
+ }
+
+ @RequiresApi(34)
+ override fun sendBroadcast(intent: Intent, receiverPermission: String?, options: Bundle?) =
+ wrappedContext.sendBroadcast(intent, receiverPermission, getDefaultBundle(options))
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContext.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContext.kt
new file mode 100644
index 0000000000..3d3ad99576
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContext.kt
@@ -0,0 +1,427 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.context
+
+import android.content.BroadcastReceiver
+import android.content.ComponentCallbacks
+import android.content.ComponentName
+import android.content.Context
+import android.content.ContextParams
+import android.content.ContextWrapper
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.ServiceConnection
+import android.content.pm.PackageManager
+import android.content.pm.ApplicationInfo
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.os.Build
+import android.os.Handler
+import android.util.Log
+import android.view.Display
+import androidx.annotation.Keep
+import com.google.android.chimera.BoundService
+import com.google.android.chimera.DynamicBroadcastReceiver
+import com.google.android.chimera.annotation.ChimeraApiVersion
+import com.google.android.chimera.loader.ChimeraModuleApk
+
+@Keep
+open class ModuleContext private constructor() : ContextThemeWrapper(null) {
+
+ companion object {
+ private const val TAG = "ModuleContext"
+ private val sensorServiceLock = Any()
+
+ @JvmStatic
+ fun createApkApplicationContext(
+ context: Context,
+ chimeraModuleApk: ChimeraModuleApk,
+ resources: Resources?,
+ classLoader: ClassLoader,
+ fulfilledApis: Map,
+ ): ModuleContext {
+ val newModuleContext = ModuleContext()
+ newModuleContext.initializeModuleContext(
+ context,
+ newModuleContext,
+ chimeraModuleApk,
+ null,
+ -1,
+ null,
+ "apkappcontext",
+ resources,
+ classLoader,
+ fulfilledApis,
+ true
+ )
+ return newModuleContext
+ }
+
+ @JvmStatic
+ fun createModuleApplicationContext(
+ moduleContext: ModuleContext,
+ moduleId: String?,
+ moduleVersion: Int,
+ subModuleId: String?
+ ): ModuleContext {
+ val attributionTag = if (subModuleId.isNullOrEmpty()) moduleId else subModuleId
+ return ModuleContext().apply {
+ initializeModuleContext(
+ moduleContext.containerContext,
+ this,
+ moduleContext.chimeraModuleApk,
+ moduleId,
+ moduleVersion,
+ subModuleId.takeIf { !it.isNullOrEmpty() },
+ attributionTag,
+ moduleContext.resources,
+ moduleContext.moduleClassLoader,
+ moduleContext.fulfilledApis,
+ false
+ )
+ }
+ }
+
+ @JvmStatic
+ fun getModuleContext(context: Context): ModuleContext? {
+ var currentContext = context
+ while (currentContext is ContextWrapper) {
+ if (currentContext is ModuleContext) {
+ return currentContext
+ }
+ currentContext = currentContext.baseContext
+ }
+ return null
+ }
+ }
+
+ private lateinit var parentModuleContext: ModuleContext
+ private lateinit var containerContext: Context
+ private lateinit var chimeraModuleApk: ChimeraModuleApk
+ private var moduleId: String? = null
+ private var moduleVersion: Int = 0
+ private var subModuleId: String? = null
+ private var moduleResources: Resources? = null
+ private lateinit var containerResources: Resources
+ private lateinit var moduleClassLoader: ClassLoader
+ private var fulfilledApis = HashMap()
+ private var updateResourcesConfiguration = true
+
+ fun initializeModuleContext(
+ context: Context,
+ parentModuleContext: ModuleContext,
+ moduleApk: ChimeraModuleApk,
+ moduleId: String?,
+ moduleVersion: Int,
+ subModuleId: String?,
+ attributionTag: String?,
+ resources: Resources?,
+ classLoader: ClassLoader,
+ fulfilledApis: Map,
+ updateResourcesConfiguration: Boolean
+ ) {
+ val superContext: Context? = when {
+ attributionTag == null -> context
+ else -> {
+ val modContext = getModuleContext(context)
+ modContext?.createAttributionModuleContext(attributionTag)
+ ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ context.createAttributionContext(attributionTag)
+ } else {
+ null
+ }
+ }
+ }
+
+ this.attachBaseContext(superContext)
+ this.parentModuleContext = parentModuleContext
+ this.containerContext = context
+ this.chimeraModuleApk = moduleApk
+ this.moduleId = moduleId
+ this.moduleVersion = moduleVersion
+ this.subModuleId = subModuleId
+ this.moduleClassLoader = classLoader
+ this.fulfilledApis = HashMap(fulfilledApis)
+ this.moduleResources = resources
+ this.containerResources = context.resources
+ this.updateResourcesConfiguration = updateResourcesConfiguration
+ if (moduleResources != null && updateResourcesConfiguration) {
+ moduleResources?.updateConfiguration(containerResources.configuration, containerResources.displayMetrics)
+ }
+ }
+
+ protected constructor(
+ context: Context,
+ moduleContext: ModuleContext,
+ moduleResources: Resources?,
+ updateResourcesConfiguration: Boolean
+ ) : this(
+ context, moduleContext,
+ if (moduleContext.subModuleId == null) {
+ moduleContext.moduleId
+ } else {
+ moduleContext.subModuleId
+ },
+ moduleResources, updateResourcesConfiguration
+ )
+
+ constructor(
+ context: Context,
+ moduleContext: ModuleContext,
+ moduleId: String?,
+ moduleVersion: Int,
+ subModuleId: String?,
+ resources: Resources?
+ ) : this() {
+ val attributionTag = if (subModuleId.isNullOrEmpty()) moduleId else subModuleId
+ initializeModuleContext(
+ context,
+ moduleContext,
+ moduleContext.chimeraModuleApk,
+ moduleId,
+ moduleVersion,
+ subModuleId.takeIf { !it.isNullOrEmpty() },
+ attributionTag,
+ resources,
+ moduleContext.moduleClassLoader,
+ moduleContext.fulfilledApis,
+ true
+ )
+ }
+
+ protected constructor(
+ context: Context,
+ moduleContext: ModuleContext,
+ attributionTag: String?,
+ moduleResources: Resources?,
+ updateResourcesConfiguration: Boolean
+ ) : this() {
+ initializeModuleContext(
+ context,
+ moduleContext.parentModuleContext,
+ moduleContext.chimeraModuleApk,
+ moduleContext.moduleId,
+ moduleContext.moduleVersion,
+ moduleContext.subModuleId,
+ attributionTag,
+ moduleResources,
+ moduleContext.moduleClassLoader,
+ moduleContext.fulfilledApis,
+ updateResourcesConfiguration
+ )
+ }
+
+ private fun createAttributionModuleContext(attributionTag: String?): ModuleContext {
+ val ctx = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ super.createAttributionContext(attributionTag)
+ } else {
+ this
+ }
+
+ return ModuleContext(
+ ctx,
+ this,
+ null,
+ this.moduleResources,
+ this.updateResourcesConfiguration
+ )
+ }
+
+ private fun configureBroadcastReceiver(receiver: BroadcastReceiver?) {
+ if (receiver is DynamicBroadcastReceiver) {
+ (receiver as DynamicBroadcastReceiver).setModuleContext(this)
+ }
+ }
+
+ override fun createAttributionContext(attributionTag: String?): Context {
+ val tag = if (moduleId == null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ getAttributionTag()
+ } else {
+ attributionTag
+ }
+ } else {
+ attributionTag
+ }
+
+ if (tag != null && tag == attributionTag) {
+ Log.w(TAG, "Attribution tag: $attributionTag ignored, replaced with: $tag")
+ }
+ return createAttributionModuleContext(attributionTag)
+ }
+
+ override fun createConfigurationContext(config: Configuration): Context {
+ if (moduleResources == null) {
+ return ModuleContext(super.createConfigurationContext(config), this, null, false)
+ }
+
+ try {
+ return ModuleContext(super.createConfigurationContext(config), this, chimeraModuleApk.getResources(), false)
+ } catch (e: Exception) {
+ throw IllegalStateException("Unable to create Resources for module $chimeraModuleApk", e)
+ }
+ }
+
+ override fun createContext(contextParams: ContextParams): Context {
+ return ModuleContext(super.createContext(contextParams), this, moduleResources, updateResourcesConfiguration)
+ }
+
+ override fun createDeviceProtectedStorageContext(): Context {
+ return ModuleContext(super.createDeviceProtectedStorageContext(), this, moduleResources, updateResourcesConfiguration)
+ }
+
+ override fun createDisplayContext(display: Display): Context {
+ if (moduleResources == null) {
+ return ModuleContext(super.createDisplayContext(display), this, null, true)
+ }
+
+ try {
+ return ModuleContext(super.createDisplayContext(display), this, chimeraModuleApk.getResources(), true)
+ } catch (e: Exception) {
+ throw RuntimeException("Failed to create module Resources", e)
+ }
+ }
+
+ override fun getApplicationContext(): Context {
+ return parentModuleContext
+ }
+
+ override fun getBaseContext(): Context {
+ return this.containerContext
+ }
+
+ override fun getClassLoader(): ClassLoader {
+ return this.moduleClassLoader
+ }
+
+ fun getContainerContext(): Context {
+ return this.containerContext
+ }
+
+ fun getContainerResources(): Resources {
+ return this.containerResources
+ }
+
+ fun getFulfilledApis(): Map {
+ return this.fulfilledApis
+ }
+
+ fun getModuleApk(): ChimeraModuleApk {
+ return this.chimeraModuleApk
+ }
+
+ fun getModuleId(): String? {
+ return this.moduleId
+ }
+
+ fun getModuleVersion(): Int {
+ return this.moduleVersion
+ }
+
+ override fun getResources(): Resources {
+ return this.moduleResources ?: this.containerResources
+ }
+
+ fun getSubmoduleId(): String? {
+ return this.subModuleId
+ }
+
+ override fun getSystemService(name: String): Any? {
+ return when (name) {
+ "sensor" -> {
+ synchronized(sensorServiceLock) {
+ super.getSystemService(name)
+ }
+ }
+
+ "window" -> {
+ containerContext.getSystemService(name)
+ }
+
+ "user", "wifi", "connectivity" -> {
+ parentModuleContext.baseContext.getSystemService(name)
+ }
+
+ else -> super.getSystemService(name)
+ }
+ }
+
+ override fun registerComponentCallbacks(callbacks: ComponentCallbacks?) {
+ super.getApplicationContext().registerComponentCallbacks(callbacks)
+ }
+
+ override fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter?): Intent? {
+ configureBroadcastReceiver(receiver)
+ return super.registerReceiver(receiver, filter)
+ }
+
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter?,
+ flags: Int
+ ): Intent? {
+ configureBroadcastReceiver(receiver)
+ return super.registerReceiver(receiver, filter, flags)
+ }
+
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter?,
+ broadcastPermission: String?,
+ scheduler: Handler?
+ ): Intent? {
+ configureBroadcastReceiver(receiver)
+ return super.registerReceiver(receiver, filter, broadcastPermission, scheduler)
+ }
+
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter?,
+ broadcastPermission: String?,
+ scheduler: Handler?,
+ flags: Int
+ ): Intent? {
+ configureBroadcastReceiver(receiver)
+ return super.registerReceiver(receiver, filter, broadcastPermission, scheduler, flags)
+ }
+
+ override fun getPackageManager(): PackageManager {
+ return ModulePackageManager(containerContext.packageManager)
+ }
+
+ override fun startService(service: Intent?): ComponentName? {
+ Log.d(TAG, "startService: $service")
+ if (service != null) {
+ val chimIntent = resolveChimeraServiceIntent(service)
+ if (chimIntent != null) return containerContext.startService(chimIntent)
+ }
+ return containerContext.startService(service)
+ }
+
+ override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean {
+ Log.d(TAG, "bindService: $service flags=$flags")
+ val chimIntent = resolveChimeraServiceIntent(service)
+ if (chimIntent != null) return containerContext.bindService(chimIntent, conn, flags)
+ return containerContext.bindService(service, conn, flags)
+ }
+
+ private fun resolveChimeraServiceIntent(intent: Intent): Intent? {
+ val action = intent.action ?: return null
+ if (action.startsWith("com.google.android.chimera.")) return null // already chimera
+ return BoundService.getStartIntent(containerContext, action)
+ }
+
+ override fun unregisterComponentCallbacks(callbacks: ComponentCallbacks?) {
+ super.getApplicationContext().unregisterComponentCallbacks(callbacks)
+ }
+
+ override fun unregisterReceiver(receiver: BroadcastReceiver?) {
+ if ((receiver is DynamicBroadcastReceiver)) {
+ (receiver as DynamicBroadcastReceiver).setModuleContext(null)
+ }
+
+ super.unregisterReceiver(receiver)
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContextRef.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContextRef.kt
new file mode 100644
index 0000000000..c86719e160
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContextRef.kt
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.context
+
+import com.google.android.chimera.loader.ChimeraModuleApk
+
+data class ModuleContextRef(
+ val moduleName: String?,
+ val moduleApiClassname: String,
+ val chimeraModuleApk: ChimeraModuleApk,
+ val moduleContext: ModuleContext,
+ val classLoader: ClassLoader,
+ val apkSha256: String? = null
+)
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModulePackageManager.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModulePackageManager.kt
new file mode 100644
index 0000000000..5da824ccea
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModulePackageManager.kt
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.chimera.context
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.util.Log
+import org.microg.gms.utils.PackageManagerWrapper
+
+class ModulePackageManager(
+ private val delegate: PackageManager
+) : PackageManagerWrapper(delegate) {
+
+ companion object {
+ private const val TAG = "ModulePackageManager"
+
+ private const val PRIVILEGED_FLAGS = (
+ 0x00100000 // MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS
+ or 0x00200000 // MATCH_INSTANT
+ or 0x20000000 // MATCH_APEX
+ )
+ }
+
+ override fun getApplicationInfo(packageName: String, flags: Int): ApplicationInfo {
+ try {
+ return delegate.getApplicationInfo(packageName, flags)
+ } catch (e: PackageManager.NameNotFoundException) {
+ val strippedFlags = flags and PRIVILEGED_FLAGS.inv()
+ if (strippedFlags != flags) {
+ Log.d(TAG, "getApplicationInfo($packageName) failed with flags 0x${Integer.toHexString(flags)}, retrying with 0x${Integer.toHexString(strippedFlags)}")
+ return delegate.getApplicationInfo(packageName, strippedFlags)
+ }
+ throw e
+ }
+ }
+
+ override fun getPackageInfo(packageName: String, flags: Int): PackageInfo {
+ try {
+ return delegate.getPackageInfo(packageName, flags)
+ } catch (e: PackageManager.NameNotFoundException) {
+ val strippedFlags = flags and PRIVILEGED_FLAGS.inv()
+ if (strippedFlags != flags) {
+ Log.d(TAG, "getPackageInfo($packageName) failed with flags 0x${Integer.toHexString(flags)}, retrying with 0x${Integer.toHexString(strippedFlags)}")
+ return delegate.getPackageInfo(packageName, strippedFlags)
+ }
+ throw e
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ApkInfoKey.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ApkInfoKey.kt
new file mode 100644
index 0000000000..bd1e87335c
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ApkInfoKey.kt
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.loader
+
+import com.google.android.chimera.config.registry.ApkRegistry
+import com.google.android.chimera.config.registry.ApkType
+
+class ApkInfoKey(val apkInfo: ApkRegistry.ApkInfo) {
+ private val hashCode = when (apkInfo.apkType) {
+ ApkType.CONTAINER -> listOf(
+ apkInfo.apkType,
+ apkInfo.versionCode,
+ apkInfo.packageName,
+ ).hashCode()
+ else -> listOf(
+ apkInfo.apkType,
+ apkInfo.versionCode,
+ apkInfo.sourceUri,
+ apkInfo.moduleName,
+ apkInfo.moduleVersion,
+ apkInfo.apkSha256,
+ ).hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ApkInfoKey) return false
+
+ if (apkInfo.apkType != other.apkInfo.apkType) return false
+ if (apkInfo.versionCode != other.apkInfo.versionCode) return false
+
+ return when (apkInfo.apkType) {
+ ApkType.CONTAINER -> {
+ apkInfo.packageName == other.apkInfo.packageName
+ }
+ else -> {
+ apkInfo.sourceUri == other.apkInfo.sourceUri
+ && apkInfo.moduleName == other.apkInfo.moduleName
+ && apkInfo.moduleVersion == other.apkInfo.moduleVersion
+ && apkInfo.apkSha256 == other.apkInfo.apkSha256
+ }
+ }
+ }
+
+ override fun hashCode(): Int {
+ return hashCode
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/BaseFileModuleApk.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/BaseFileModuleApk.kt
new file mode 100644
index 0000000000..cb79ec0479
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/BaseFileModuleApk.kt
@@ -0,0 +1,70 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.loader
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.res.Resources
+import android.content.res.loader.ResourcesLoader
+import android.content.res.loader.ResourcesProvider
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import androidx.annotation.RequiresApi
+import java.io.File
+import java.io.IOException
+
+abstract class BaseFileModuleApk(
+ context: Context,
+ moduleVersion: Int,
+ moduleType: Int
+) : ChimeraModuleApk(context, moduleVersion, moduleType) {
+ private var cachedApplicationInfo: ApplicationInfo? = null
+ private var resourcesLoader: ResourcesLoader? = null
+
+ private fun getApkPath(): String {
+ val path = getArchiveFilePath() ?: throw NameNotFoundException("Could not find APK path for ${toString()}")
+ return path
+ }
+
+ override fun getApplicationInfo(): ApplicationInfo {
+ synchronized(this) {
+ if (cachedApplicationInfo == null) {
+ cachedApplicationInfo = ApplicationInfo().apply {
+ packageName = appContext.packageName
+ publicSourceDir = getApkPath()
+ sourceDir = getApkPath()
+ }
+ }
+ return cachedApplicationInfo!!
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ override fun getResources(): Resources {
+ val resources = appContext.packageManager.getResourcesForApplication("android")
+ resources.addLoaders(createResourcesLoader(listOf(getApkPath())))
+ return resources
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ protected fun createResourcesLoader(paths: List): ResourcesLoader {
+ synchronized(this) {
+ if (resourcesLoader == null) {
+ val resourcesLoader = ResourcesLoader()
+ paths.forEach { path ->
+ try {
+ val parcelFileDescriptor = ParcelFileDescriptor.open(File(path), ParcelFileDescriptor.MODE_READ_ONLY)
+ resourcesLoader.addProvider(ResourcesProvider.loadFromApk(parcelFileDescriptor))
+ } catch (e: IOException) {
+ throw Exception("error: could not open file:$path ${File(path).exists()}-> ${e.message}", e)
+ }
+ }
+ this.resourcesLoader = resourcesLoader
+ }
+ return resourcesLoader!!
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraApkLoader.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraApkLoader.kt
new file mode 100644
index 0000000000..5e16cb4ef0
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraApkLoader.kt
@@ -0,0 +1,157 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.loader
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.util.Log
+import com.google.android.chimera.component.ContainerApk
+import com.google.android.chimera.config.InvalidConfigException
+import com.google.android.chimera.config.registry.ApkRegistry
+import com.google.android.chimera.config.registry.ApkType
+import com.google.android.chimera.container.ModuleApi
+import com.google.android.chimera.context.ModuleContext
+import com.google.android.chimera.context.ModuleContextRef
+import java.util.concurrent.ConcurrentHashMap
+
+object ChimeraApkLoader {
+ private const val TAG = "ChimeraApkLoader"
+ private val moduleApiCache = HashMap()
+ private val moduleContextCache = ConcurrentHashMap()
+
+ fun getModuleApi(name: String): ModuleApi? {
+ synchronized(moduleApiCache) {
+ var moduleApi = moduleApiCache[name]
+
+ if (moduleApi == null) {
+ try {
+ moduleApi = Class.forName(name).asSubclass(ModuleApi::class.java).getConstructor().newInstance()
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to instantiate chimera ModuleApi class : $name", e)
+ }
+
+ if (moduleApi != null) {
+ moduleApiCache[name] = moduleApi
+ }
+ }
+ return moduleApi
+ }
+ }
+
+ fun loadModule(context: Context, apkInfo: ApkRegistry.ApkInfo): ModuleContextRef {
+ val cacheKey = ApkInfoKey(apkInfo)
+ moduleContextCache[cacheKey]?.let { return it }
+
+ val dependencyLength = apkInfo.dependCount
+ val isDependLoop = dependencyLength == 1 && apkInfo.moduleName == apkInfo.dependModuleNames[0]
+ lateinit var moduleContextRef: ModuleContextRef
+ if (dependencyLength != 0 && !isDependLoop) {
+ val apiClassName = apkInfo.moduleApiClassName
+ val moduleApi = getModuleApi(apiClassName)
+ require(moduleApi != null) { "failed to get module api: $apiClassName" }
+
+ val parentClassLoader: ClassLoader = context.applicationContext.classLoader
+ for (i in dependencyLength - 1 downTo 0) {
+ Log.d(TAG, "dependencyApkIndex: ${apkInfo.dependModuleNames[i]}")
+ }
+
+ val moduleApk = ChimeraModuleApk.createModuleApk(context, apkInfo)
+ require(moduleApk != null) { "failed to create ModuleApk" }
+
+ Log.d(TAG, "skip APK signature verification")
+
+ try {
+ moduleApi.onBeforeApkLoad(context, moduleApk)
+ } catch (e: Exception) {
+ Log.w(TAG, "Setup failed for module ${apkInfo.apkPath}", e)
+ throw Exception("onBeforeApkLoad failed.", e)
+ }
+
+ val newModuleClassLoader: ClassLoader?
+ try {
+ newModuleClassLoader = moduleApk.createClassLoader(parentClassLoader)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Config is out of date: $moduleApk has been removed")
+ throw InvalidConfigException("can\'t load code from $moduleApk", e)
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Failed to load code for module $moduleApk", e)
+ throw RuntimeException("Failed to create ClassLoader for module $moduleApk", e)
+ }
+ Log.d(TAG, "loadModule: " + context.packageName)
+ val newModuleContext = ModuleContext.createApkApplicationContext(
+ context.applicationContext,
+ moduleApk,
+ getModuleResources(moduleApk),
+ newModuleClassLoader,
+ emptyMap()
+ )
+
+ try {
+ moduleApi.onApkLoaded(newModuleContext)
+ } catch (e: Exception) {
+ Log.w(TAG, "Initialization failed for module apk ${apkInfo.apkPath}", e)
+ throw Exception("onApkLoaded failed.", e)
+ }
+
+ moduleContextRef = ModuleContextRef(
+ apkInfo.moduleName,
+ apkInfo.moduleApiClassName,
+ moduleApk,
+ newModuleContext,
+ newModuleClassLoader,
+ apkInfo.apkSha256.ifEmpty { null }
+ )
+ } else {
+ require(apkInfo.apkType == ApkType.CONTAINER) {
+ "Unexpected apkType for ${apkInfo.packageName}: expected CONTAINER but got ${apkInfo.apkType}"
+ }
+ val containerApk = ContainerApk(context)
+ val moduleAppContext = ModuleContext.createApkApplicationContext(
+ context,
+ containerApk,
+ null,
+ context.applicationContext.classLoader,
+ emptyMap()
+ )
+
+ moduleContextRef = ModuleContextRef(
+ apkInfo.moduleName,
+ apkInfo.moduleApiClassName,
+ containerApk,
+ moduleAppContext,
+ context.classLoader,
+ apkInfo.apkSha256.ifEmpty { null }
+ )
+ }
+
+ moduleContextCache[cacheKey] = moduleContextRef
+ return moduleContextRef
+ }
+
+ fun clearModuleCaches(moduleName: String? = null, apkPath: String? = null) {
+ if (moduleName.isNullOrEmpty() && apkPath.isNullOrEmpty()) return
+ moduleContextCache.entries.removeAll { entry ->
+ val info = entry.key.apkInfo
+ (!moduleName.isNullOrEmpty() && info.moduleName == moduleName) ||
+ (!apkPath.isNullOrEmpty() && info.apkPath == apkPath)
+ }
+ }
+ fun getModuleResources(chimeraModuleApk: ChimeraModuleApk): Resources? {
+ if (chimeraModuleApk is ContainerApk) {
+ return null
+ }
+
+ try {
+ return chimeraModuleApk.getResources()
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Config is out of date: $chimeraModuleApk has been removed")
+ throw InvalidConfigException("can\'t load resources from $chimeraModuleApk", e)
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Failed to load resources for module $chimeraModuleApk", e)
+ throw e
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleApk.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleApk.kt
new file mode 100644
index 0000000000..20eebcd6f2
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleApk.kt
@@ -0,0 +1,72 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.loader
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.res.Resources
+import android.util.Log
+import com.google.android.chimera.component.ChimeraFileApk
+import com.google.android.chimera.component.ContainerApk
+import com.google.android.chimera.config.registry.ApkRegistry
+import com.google.android.chimera.config.registry.ApkType
+
+abstract class ChimeraModuleApk(
+ val context: Context,
+ val apkType: Int,
+ val moduleType: Int
+) {
+ protected var appContext: Context = context.applicationContext
+
+ abstract fun getApplicationInfo(): ApplicationInfo
+
+ abstract fun createClassLoader(parentClassLoader: ClassLoader): ClassLoader
+
+ abstract fun getArchiveFilePath(): String?
+
+ open fun getResources(): Resources {
+ try {
+ val resources = appContext.packageManager.getResourcesForApplication(getApplicationInfo())
+ if (resources.assets == null) {
+ throw IllegalArgumentException("Resources is null")
+ }
+ return resources
+ } catch (e: Exception) {
+ throw IllegalArgumentException("Error in getResources()", e)
+ }
+ }
+
+ companion object {
+ private const val TAG = "ChimeraModuleApk"
+
+ @JvmStatic
+ fun createModuleApk(context: Context, apkInfo: ApkRegistry.ApkInfo): ChimeraModuleApk? {
+ // A module descriptor must carry either both a name and a version, or neither.
+ if (apkInfo.moduleName.isEmpty() != apkInfo.moduleVersion.isEmpty()) {
+ Log.d(TAG, "Invalid module.yaml info for apk: ${apkInfo.moduleName}")
+ return null
+ }
+
+ when (apkInfo.apkType) {
+ ApkType.CONTAINER -> {
+ if ("com.google.android.gms" != apkInfo.packageName) {
+ Log.w(TAG, "Unable to create ModuleApk from invalid descriptor (CONTAINER has incorrect package name)")
+ return null
+ }
+ return ContainerApk(context)
+ }
+ ApkType.FILE -> {
+ Log.d(TAG, "Creating ChimeraFileApk: moduleType=${apkInfo.moduleType}, apkPath=${apkInfo.apkPath}")
+ return ChimeraFileApk(context, apkInfo.moduleType, apkInfo.apkPath, apkInfo.apkSha256.ifEmpty { null })
+ }
+ else -> {
+ Log.d(TAG, "Module APK type not supported")
+ return null
+ }
+ }
+ }
+ }
+
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleLdr.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleLdr.kt
new file mode 100644
index 0000000000..c56613c560
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleLdr.kt
@@ -0,0 +1,326 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.loader
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import com.google.android.chimera.config.ChimeraApkManifestReader
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.ChimeraModuleCapabilities
+import com.google.android.chimera.config.ChimeraStorage
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.config.InvalidConfigException
+import com.google.android.chimera.config.registry.ApkRegistry
+import com.google.android.chimera.config.registry.DynamicModuleRegistry
+import com.google.android.chimera.context.ModuleContext
+import com.google.android.chimera.context.ModuleContextRef
+import java.io.File
+import java.io.FileOutputStream
+import java.security.MessageDigest
+import java.util.UUID
+import java.util.zip.ZipFile
+import androidx.core.net.toUri
+
+object ChimeraModuleLdr {
+ val loaderLock = Any()
+ val loaderModuleContext = java.util.concurrent.ConcurrentHashMap()
+
+ private const val TAG = "ChimeraModuleLdr"
+
+ fun createModuleContext(
+ context: Context,
+ moduleId: String,
+ moduleName: String?,
+ moduleVersion: Int,
+ moduleContextRef: ModuleContextRef
+ ): Context {
+ val moduleContext = moduleContextRef.moduleContext
+ val resources = ChimeraApkLoader.getModuleResources(moduleContext.getModuleApk())
+ val effectiveModuleName = moduleName?.takeIf { it.isNotEmpty() }
+ return ModuleContext(context, moduleContext, moduleId, moduleVersion, effectiveModuleName, resources)
+ }
+
+ fun getOrCreateModuleContext(
+ context: Context,
+ moduleId: String,
+ moduleVersion: Int,
+ apkInfo: ApkRegistry.ApkInfo,
+ implClassName: String?
+ ): ModuleContextRef {
+ Log.d(TAG, "getOrCreateModuleContext")
+
+ val apkModuleContextRef = loadApkModuleContextRef(context, apkInfo)
+
+ loaderModuleContext[moduleId]?.let {
+ Log.d(TAG, "getOrCreateModuleContext: module context exist for $moduleId")
+ val sameApk = it.chimeraModuleApk.getArchiveFilePath() == apkInfo.apkPath
+ val sameDigest = apkInfo.apkSha256.isEmpty() || it.apkSha256 == apkInfo.apkSha256
+ if (sameApk && sameDigest) return it
+ Log.w(TAG, "getOrCreateModuleContext: discarding stale module context for $moduleId")
+ loaderModuleContext.remove(moduleId)
+ }
+
+ loaderModuleContext.values.find {
+ it.chimeraModuleApk.getArchiveFilePath() == apkInfo.apkPath &&
+ (apkInfo.apkSha256.isEmpty() || it.apkSha256 == apkInfo.apkSha256)
+ }?.let { existing ->
+ Log.d(TAG, "getOrCreateModuleContext: reusing ClassLoader for $moduleId (same APK as ${existing.moduleName})")
+ loaderModuleContext[moduleId] = existing
+ return existing
+ }
+
+ val moduleContextRef = ModuleContextRef(
+ apkModuleContextRef.moduleName,
+ apkModuleContextRef.moduleApiClassname,
+ apkModuleContextRef.chimeraModuleApk,
+ ModuleContext.createModuleApplicationContext(
+ apkModuleContextRef.moduleContext,
+ moduleId,
+ moduleVersion,
+ null
+ ),
+ apkModuleContextRef.classLoader,
+ apkModuleContextRef.apkSha256
+ )
+ Log.d(TAG, "getOrCreateModuleContext: create new module context ${moduleContextRef.moduleContext}")
+ loaderModuleContext[moduleId] = moduleContextRef
+
+ val moduleApi = ChimeraApkLoader.getModuleApi(moduleContextRef.moduleApiClassname)
+ if (moduleApi == null) {
+ Log.w(TAG, "Failed to capture application context. ModuleApi not found for: ${moduleContextRef.moduleName}")
+ return moduleContextRef
+ }
+
+ if (moduleId.isNotEmpty()) {
+ try {
+ moduleApi.onModuleLoaded(moduleId, implClassName, moduleContextRef.moduleContext)
+ } catch (_: PackageManager.NameNotFoundException) {
+ Log.d(TAG, "Config is out of date: ${moduleContextRef.chimeraModuleApk} has been modified")
+ throw InvalidConfigException("Module APK has been modified: ${moduleContextRef.chimeraModuleApk}")
+ } catch (e: Exception) {
+ throw IllegalStateException("Failed to set module context for $moduleId", e)
+ }
+ }
+
+ return moduleContextRef
+ }
+
+ private fun loadApkModuleContextRef(context: Context, apkInfo: ApkRegistry.ApkInfo): ModuleContextRef {
+ return synchronized(loaderLock) { ChimeraApkLoader.loadModule(context, apkInfo) }
+ }
+
+ fun clearModuleCache(moduleId: String? = null, moduleName: String? = null, apkPath: String? = null) {
+ if (moduleId.isNullOrEmpty() && moduleName.isNullOrEmpty() && apkPath.isNullOrEmpty()) return
+ loaderModuleContext.entries.removeAll { entry ->
+ val ref = entry.value
+ (!moduleId.isNullOrEmpty() && entry.key == moduleId) ||
+ (!moduleName.isNullOrEmpty() && ref.moduleName == moduleName) ||
+ (!apkPath.isNullOrEmpty() && ref.chimeraModuleApk.getArchiveFilePath() == apkPath)
+ }
+ ChimeraApkLoader.clearModuleCaches(moduleName, apkPath)
+ }
+
+ fun loadModule(context: Context, moduleId: String, moduleName: String?, moduleVersion: Int): Context? {
+ if (!DynamicModuleSettings.isAvailable(context)) {
+ Log.d(TAG, "Dynamic modules unavailable; refusing to load $moduleId")
+ return null
+ }
+ val moduleEntry = DynamicModuleRegistry.getByModuleId(moduleId)
+ val targetModuleName = moduleEntry?.moduleName ?: moduleName
+ if (targetModuleName == null) {
+ Log.w(TAG, "Module not found in registry: $moduleId")
+ return null
+ }
+
+ val apkInfo = resolveApkInfo(context, moduleId, targetModuleName)
+ ?: resolveApkInfoViaContentProvider(context, moduleId, targetModuleName)
+ if (apkInfo == null) {
+ Log.w(TAG, "APK not found for module: $targetModuleName")
+ return null
+ }
+ val effectiveModuleVersion = moduleVersion.takeIf { it > 0 }
+ ?: apkInfo.moduleVersion.toIntOrNull()?.takeIf { it > 0 }
+ ?: 0
+
+ // implClassName stays null so GmsModuleApi.onModuleLoaded derives the AppContextProvider class
+ // name by the standard naming convention (chimera.modules..AppContextProvider),
+ // matching official GMS which carries no hardcoded moduleId->class table.
+ val implClassName: String? = null
+ val ref = getOrCreateModuleContext(
+ context,
+ moduleId,
+ effectiveModuleVersion,
+ apkInfo,
+ implClassName
+ )
+ return createModuleContext(context, moduleId, moduleName, effectiveModuleVersion, ref)
+ }
+
+ private fun resolveApkInfo(context: Context, moduleId: String, moduleName: String): ApkRegistry.ApkInfo? {
+ val chimeraModule = ChimeraConfigManager.findModule(moduleId, moduleName)
+
+ if (chimeraModule != null && !chimeraModule.installedApkPath.isNullOrEmpty()) {
+ val protoPath = chimeraModule.installedApkPath!!
+ val verifiedApk = ChimeraStorage.verifiedModuleApk(
+ context = context,
+ file = File(protoPath),
+ expectedModuleName = null,
+ expectedSha256 = chimeraModule.apkSha256,
+ )
+ if (verifiedApk != null) {
+ Log.d(TAG, "Resolved verified APK for module=$moduleId")
+ val configuredModuleId = chimeraModule.moduleId?.takeIf { it.isNotEmpty() } ?: moduleId
+ val capability = readModuleCapability(verifiedApk, configuredModuleId)
+ val apiClass = capability?.initializerMode?.moduleApiClassName
+ if (apiClass == null) {
+ Log.w(TAG, "Configured APK has unsupported module API for $configuredModuleId: ${capability?.requiredApis}")
+ return null
+ }
+ return ApkRegistry.createDynamicApkInfo(
+ verifiedApk.absolutePath,
+ moduleName,
+ chimeraModule.moduleVersion.orEmpty(),
+ apiClass,
+ apkSha256 = chimeraModule.apkSha256.orEmpty()
+ )
+ }
+ Log.w(TAG, "Configured APK is missing or no longer trusted for $moduleId")
+ }
+ return null
+ }
+
+ private fun resolveApkInfoViaContentProvider(context: Context, moduleId: String, moduleName: String): ApkRegistry.ApkInfo? {
+ try {
+ val appContext = context.applicationContext ?: context
+ val resolver = appContext.contentResolver ?: return null
+ val metadata = queryProviderMetadata(resolver, moduleId) ?: return null
+ val cacheDir = File(appContext.cacheDir, "chimera_modules")
+ val safeId = moduleId.replace(Regex("[^A-Za-z0-9_.-]"), "_").take(160)
+ val digestToken = metadata.sha256.take(16)
+ val cachedApk = File(cacheDir, "$safeId-${metadata.version}-$digestToken.apk")
+ if (cachedApk.exists() && cachedApk.length() > 0) {
+ if (isValidApk(cachedApk) && matchesExpectedDigest(cachedApk, metadata.sha256)) {
+ val capability = readModuleCapability(cachedApk, moduleId)
+ if (capability?.moduleVersion?.toString() == metadata.version) {
+ Log.d(TAG, "Using cached APK for module=$moduleId")
+ return ApkRegistry.createDynamicApkInfo(
+ cachedApk.absolutePath, moduleName, metadata.version,
+ capability.initializerMode.moduleApiClassName!!,
+ apkSha256 = metadata.sha256
+ )
+ }
+ Log.w(TAG, "Cached APK has no supported Chimera capability for module=$moduleId")
+ cachedApk.delete()
+ } else {
+ Log.w(TAG, "Cached APK is invalid for module=$moduleId")
+ cachedApk.delete()
+ }
+ }
+
+ val uri = "content://com.google.android.gms.chimera/module_apk/$moduleId".toUri()
+ val pfd = try {
+ resolver.openFileDescriptor(uri, "r")
+ } catch (e: Exception) {
+ Log.d(TAG, "ContentProvider openFile failed for $moduleId: ${e.message}")
+ return null
+ } ?: return null
+
+ cacheDir.mkdirs()
+ val tmpFile = File(cacheDir, "$safeId.tmp-${UUID.randomUUID()}")
+ try {
+ ParcelFileDescriptor.AutoCloseInputStream(pfd).use { input ->
+ FileOutputStream(tmpFile).use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ if (!isValidApk(tmpFile) || !matchesExpectedDigest(tmpFile, metadata.sha256)) {
+ Log.w(TAG, "ContentProvider served invalid APK for $moduleName (${tmpFile.length()} bytes)")
+ tmpFile.delete()
+ return null
+ }
+
+ val copiedCapability = readModuleCapability(tmpFile, moduleId)
+ if (copiedCapability?.moduleVersion?.toString() != metadata.version) {
+ Log.w(TAG, "Provider APK capability mismatch for $moduleId: expected=${metadata.version}")
+ tmpFile.delete()
+ return null
+ }
+ if (!tmpFile.renameTo(cachedApk)) {
+ tmpFile.delete()
+ Log.w(TAG, "Failed to rename cached APK for $moduleName")
+ return null
+ }
+
+ Log.d(TAG, "Cached verified APK for module=$moduleId version=${metadata.version}")
+ val moduleVersion = metadata.version
+ cacheDir.listFiles()?.filter {
+ it.isFile && it.name.startsWith("$safeId-") && it != cachedApk
+ }?.forEach(File::delete)
+ return ApkRegistry.createDynamicApkInfo(
+ cachedApk.absolutePath, moduleName, moduleVersion,
+ copiedCapability.initializerMode.moduleApiClassName!!,
+ apkSha256 = metadata.sha256
+ )
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to cache APK via ContentProvider for $moduleName", e)
+ tmpFile.delete()
+ runCatching { pfd.close() }
+ return null
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "ContentProvider-based APK resolution failed for $moduleId: ${e.message}")
+ return null
+ }
+ }
+
+ private data class ProviderApkMetadata(val version: String, val sha256: String)
+
+ private fun queryProviderMetadata(
+ resolver: android.content.ContentResolver,
+ moduleId: String,
+ ): ProviderApkMetadata? {
+ val uri = "content://com.google.android.gms.chimera/api/$moduleId".toUri()
+ return resolver.query(uri, null, null, null, null)?.use { cursor ->
+ if (!cursor.moveToFirst()) return@use null
+ val version = cursor.getLong(cursor.getColumnIndexOrThrow("version"))
+ .takeIf { it > 0 }
+ ?.toString()
+ ?: return@use null
+ val shaIndex = cursor.getColumnIndex("apkSha256")
+ val sha256 = if (shaIndex >= 0) cursor.getString(shaIndex).orEmpty() else ""
+ if (sha256.isEmpty()) null else ProviderApkMetadata(version, sha256)
+ }
+ }
+
+ private fun readModuleCapability(apkFile: File, moduleId: String): ChimeraModuleCapabilities? =
+ ChimeraApkManifestReader.readCapabilities(apkFile).firstOrNull {
+ it.moduleId == moduleId && it.initializerMode.moduleApiClassName != null
+ }
+
+ private fun matchesExpectedDigest(file: File, expected: String): Boolean {
+ if (expected.isEmpty()) return false
+ val digest = MessageDigest.getInstance("SHA-256")
+ file.inputStream().buffered().use { input ->
+ val buffer = ByteArray(8192)
+ var read: Int
+ while (input.read(buffer).also { read = it } >= 0) digest.update(buffer, 0, read)
+ }
+ val actual = digest.digest().joinToString("") { "%02x".format(it) }
+ return actual.equals(expected, ignoreCase = true)
+ }
+
+ private fun isValidApk(file: File): Boolean {
+ return try {
+ ZipFile(file).use { zip ->
+ zip.getEntry("AndroidManifest.xml") != null || zip.getEntry("classes.dex") != null
+ }
+ } catch (_: Exception) {
+ false
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraResource.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraResource.kt
new file mode 100644
index 0000000000..05080528c1
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraResource.kt
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.chimera.util
+
+import android.content.res.Resources
+import android.util.Log
+import java.util.concurrent.ConcurrentHashMap
+
+object ChimeraResource {
+ private const val TAG = "ChimeraResource"
+ private val resourceCache = ConcurrentHashMap()
+
+ private data class ResourceInfo(
+ val packageName: String,
+ val nameToIdCache: ConcurrentHashMap = ConcurrentHashMap()
+ ) {
+ companion object {
+ val MISSING = ResourceInfo(
+ packageName = "missing",
+ nameToIdCache = ConcurrentHashMap()
+ )
+ }
+ }
+
+ private fun getResourceIdForName(
+ classLoader: ClassLoader,
+ resources: Resources,
+ resourceName: String
+ ): Int {
+ val resourceInfo = resourceCache.getOrPut(classLoader) {
+ getResourcePackageName(classLoader, resources)?.let { ResourceInfo(it) } ?: ResourceInfo.MISSING
+ }
+
+ if (resourceInfo === ResourceInfo.MISSING) {
+ throw Resources.NotFoundException("Failed to get resource package name for module $resourceName")
+ }
+
+ resourceInfo.nameToIdCache[resourceName]?.let { return it }
+
+ val resourceId = try {
+ resources.getIdentifier(resourceName, null, resourceInfo.packageName)
+ } catch (e: Resources.NotFoundException) {
+ Log.e(TAG, "Unable to locate resource id for resourceName: $resourceName and resourcePackage: ${resourceInfo.packageName}", e)
+ throw e
+ }
+
+ return resourceInfo.nameToIdCache.putIfAbsent(resourceName, resourceId) ?: resourceId
+ }
+
+ private fun getResourcePackageName(classLoader: ClassLoader, resources: Resources): String? {
+ try {
+ val clazz = classLoader.loadClass("com.google.android.chimeraresources.R\$id")
+ val chimeraField = clazz.getField("chimera")
+ val chimeraResourceId = chimeraField.getInt(null)
+
+ return resources.getResourcePackageName(chimeraResourceId)
+ } catch (e: ClassNotFoundException) {
+ Log.w(TAG, "Chimera resources class not found: ${e.message}")
+ return null
+ } catch (e: NoSuchFieldException) {
+ Log.e(TAG, "The resource chimera could not be found: ${e.message}")
+ return null
+ } catch (e: IllegalAccessException) {
+ Log.e(TAG, "Failed to get resource ID for chimera: ${e.message}")
+ return null
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Invalid access to 'chimera' field: ${e.message}")
+ return null
+ } catch (e: Resources.NotFoundException) {
+ Log.w(TAG, "${e.message}")
+ return null
+ }
+ }
+
+ fun getResourceId(appClassLoader: ClassLoader, moduleResources: Resources, sourceResources: Resources, resId: Int): Int {
+ if (resId == 0) {
+ return 0
+ }
+
+ var resourceName = sourceResources.getResourceName(resId)
+ if (resourceName.startsWith("@")) {
+ resourceName = resourceName.substring(1)
+ }
+ val colonIndex = resourceName.indexOf(':')
+ if (colonIndex != -1) {
+ val packageName = resourceName.substring(0, colonIndex)
+
+ if (packageName == "android") {
+ return resId
+ }
+
+ val resourceNameOnly = resourceName.substring(colonIndex + 1)
+ return getResourceIdForName(appClassLoader, moduleResources, resourceNameOnly)
+ }
+
+ return getResourceIdForName(appClassLoader, moduleResources, resourceName)
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraViewCreator.kt b/play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraViewCreator.kt
new file mode 100644
index 0000000000..2357c0151a
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraViewCreator.kt
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.chimera.util
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewStub
+import android.view.LayoutInflater
+import java.lang.reflect.Constructor
+import java.util.concurrent.ConcurrentHashMap
+
+object ChimeraViewCreator {
+
+ private val constructorCache = ConcurrentHashMap>>()
+
+ private val CONSTRUCTOR_SIGNATURE = arrayOf(Context::class.java, AttributeSet::class.java)
+
+ fun createView(
+ moduleClassLoader: ClassLoader,
+ context: Context,
+ viewName: String,
+ attrs: AttributeSet
+ ): View? {
+ if (!viewName.contains(".")) return null
+
+ try {
+ val clCache = constructorCache.getOrPut(moduleClassLoader) { ConcurrentHashMap() }
+
+ val constructor = clCache.getOrPut(viewName) {
+ val viewClass = moduleClassLoader.loadClass(viewName).asSubclass(View::class.java)
+ viewClass.getConstructor(*CONSTRUCTOR_SIGNATURE).also {
+ it.isAccessible = true
+ }
+ }
+
+ val view = constructor.newInstance(context, attrs)
+
+ if (view is ViewStub) {
+ view.layoutInflater = LayoutInflater.from(context)
+ }
+
+ return view
+ } catch (_: Exception) {
+ return null
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/GmsApiService.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/GmsApiService.kt
new file mode 100644
index 0000000000..c7f1adb41e
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/GmsApiService.kt
@@ -0,0 +1,102 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.chimera
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.IBinder
+import android.util.Log
+import androidx.annotation.Keep
+import androidx.lifecycle.LifecycleService
+import org.microg.gms.BaseService
+
+/**
+ * GMS Service Proxy for Chimera bound services.
+ *
+ * Client sends: action="com.google.android.chimera.BoundService.START"
+ * data="chimera-action:actual.service.action"
+ *
+ * This proxy converts the Intent and routes to the appropriate service implementation
+ * using ChimeraConfigManager's service route table.
+ */
+@Keep
+open class GmsApiService : LifecycleService() {
+ private val TAG = "GmsApiService"
+
+ // Routes chimera-action to real service implementations.
+ // When a dynamically loaded module calls BoundService.getStartIntent("action"),
+ // the Intent is routed here and we look up the real service to delegate to.
+ private val fallbackServiceMap = mapOf(
+ "com.google.android.gms.chimera.container.moduleinstall.ModuleInstallService.START"
+ to "org.microg.gms.moduleinstall.ModuleInstallService",
+ "com.google.android.gms.phenotype.service.START"
+ to "org.microg.gms.phenotype.PhenotypeService",
+ "com.google.android.gms.clearcut.service.START"
+ to "org.microg.gms.clearcut.ClearcutLoggerService",
+ "com.google.android.gms.games.service.START"
+ to "org.microg.gms.games.GamesService",
+ )
+
+ override fun onBind(intent: Intent): IBinder? {
+ super.onBind(intent)
+ Log.d(TAG, "onBind intent: $intent")
+
+ val resolvedIntent = resolveChimeraIntent(intent)
+ val action = resolvedIntent.action ?: return null
+
+ val targetClassName = fallbackServiceMap[action]
+ if (targetClassName == null) {
+ Log.w(TAG, "No service found for action: $action")
+ return null
+ }
+
+ return bindToService(targetClassName, resolvedIntent)
+ }
+
+ /**
+ * Converts chimera-action Intent.
+ * Input: action="com.google.android.chimera.BoundService.START", data="chimera-action:real.action"
+ * Output: action="real.action"
+ */
+ private fun resolveChimeraIntent(intent: Intent): Intent {
+ if (intent.action != "com.google.android.chimera.BoundService.START") {
+ return intent
+ }
+
+ val uri = intent.data ?: return intent
+ val realAction = uri.schemeSpecificPart
+ if (uri.scheme == "chimera-action" && !realAction.isNullOrEmpty()) {
+ return Intent(intent).apply {
+ action = realAction
+ data = Uri.Builder().scheme("chimera-action").build()
+ }
+ }
+
+ Log.w(TAG, "Intent missing action data: $intent")
+ return intent
+ }
+
+ private fun bindToService(targetClassName: String, intent: Intent): IBinder? {
+ try {
+ val clazz = Class.forName(targetClassName)
+ val service = clazz.getDeclaredConstructor().newInstance() as? Service ?: return null
+
+ val attachMethod = Service::class.java.getDeclaredMethod("attachBaseContext", Context::class.java)
+ attachMethod.isAccessible = true
+ attachMethod.invoke(service, this)
+
+ Log.d(TAG, "Chimera bridge to $targetClassName")
+ if (service is BaseService) {
+ return service.onBind(intent)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to load chimera service: $targetClassName", e)
+ }
+ return null
+ }
+
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentApiService.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentApiService.kt
new file mode 100644
index 0000000000..568e7e25f7
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentApiService.kt
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.chimera
+
+import androidx.annotation.Keep
+
+/**
+ * Chimera bound service proxy for persistent (non-instant-app) API services.
+ *
+ * Used as a routing target for chimera bound services like phenotype.
+ *
+ * When a dynamically loaded module calls:
+ * BoundService.getStartIntent(context, "com.google.android.gms.phenotype.service.START")
+ * The Intent is routed to this service class via chimera-action scheme,
+ * which then delegates to the real PhenotypeService implementation.
+ */
+@Keep
+class PersistentApiService : GmsApiService()
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentDirectBootAwareApiService.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentDirectBootAwareApiService.kt
new file mode 100644
index 0000000000..de8a8bc8cd
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentDirectBootAwareApiService.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.chimera
+
+import androidx.annotation.Keep
+
+/**
+ * Chimera bound service proxy for persistent direct-boot-aware API services.
+ *
+ * Used as a routing target for chimera bound services like clearcut.
+ *
+ * Same as PersistentApiService but intended for services that need to run
+ * before the user unlocks the device (direct boot mode).
+ */
+@Keep
+class PersistentDirectBootAwareApiService : GmsApiService()
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleApi.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleApi.kt
new file mode 100644
index 0000000000..5db256d4e7
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleApi.kt
@@ -0,0 +1,57 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.chimera.container
+
+import android.content.Context
+import android.util.Log
+import androidx.annotation.Keep
+import com.google.android.chimera.loader.ChimeraModuleApk
+import com.google.android.chimera.component.ChimeraFileApk
+import com.google.android.chimera.container.ModuleApi
+import java.lang.reflect.Method
+
+@Keep
+class DynamiteModuleApi : ModuleApi() {
+ private val TAG = "DynamiteModuleApi"
+
+ override fun onApkLoaded(context: Context) {
+ val classLoader = context.classLoader
+ var methodV2: Method? = null
+ var methodV1: Method? = null
+
+ try {
+ val clazz = classLoader.loadClass("com.google.android.gms.chimera.DynamiteModuleInitializer")
+
+ try {
+ methodV2 = clazz.getDeclaredMethod("initializeModuleV2", Context::class.java, Boolean::class.javaPrimitiveType)
+ } catch (_: NoSuchMethodException) {
+ try {
+ methodV1 = clazz.getDeclaredMethod("initializeModuleV1", Context::class.java)
+ } catch (_: NoSuchMethodException) {
+ }
+ }
+
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to set dynamite application context: ${e}")
+ return
+ }
+
+ if (methodV2 != null) {
+ invokeStaticMethod(methodV2, arrayOf(context, false))
+ return
+ }
+
+ if (methodV1 != null) {
+ invokeStaticMethod(methodV1, arrayOf(context))
+ }
+ }
+
+ override fun onBeforeApkLoad(context: Context, moduleApk: ChimeraModuleApk) {
+ if (moduleApk is ChimeraFileApk) {
+ moduleApk.className = "com.google.android.gms.chimera.DynamiteModuleInitializer"
+ moduleApk.apkPath = moduleApk.getFullApkPath()
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/GmsModuleApi.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/GmsModuleApi.kt
new file mode 100644
index 0000000000..9545682864
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/GmsModuleApi.kt
@@ -0,0 +1,59 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.chimera.container
+
+import android.content.Context
+import android.util.Log
+import androidx.annotation.Keep
+import com.google.android.chimera.loader.ChimeraModuleApk
+import com.google.android.chimera.container.ModuleApi
+import com.google.android.gms.common.app.BaseApplicationContext
+import com.google.android.gms.common.app.GCoreApplicationContext
+
+@Keep
+class GmsModuleApi : ModuleApi() {
+ private val TAG = "GmsModuleApi"
+ override fun onApkLoaded(context: Context) {
+ invokeStaticMethod(
+ context.classLoader.loadClass("com.google.android.gms.chimera.GmsModuleInitializer")
+ .getMethod(
+ "initializeModuleV0",
+ Context::class.java,
+ BaseApplicationContext::class.java
+ ),
+ arrayOf(context, GCoreApplicationContext.instance)
+ )
+ }
+
+ override fun onBeforeApkLoad(context: Context, moduleApk: ChimeraModuleApk) {
+ }
+
+ override fun onModuleLoaded(moduleName: String, providerClass: String?, context: Context) {
+ val targetClassName = if (providerClass == null) {
+ val moduleKey: String = if (moduleName.startsWith("com.google.android.gms.")) {
+ moduleName.removePrefix("com.google.android.gms.")
+ } else {
+ moduleName.ifEmpty { "container" }
+ }
+
+ "com.google.android.gms.chimera.modules.${moduleKey.replace("__", ".").replace('_', '.')}.AppContextProvider"
+ } else {
+ providerClass
+ }
+
+ try {
+ val clazz = context.classLoader.loadClass(targetClassName)
+ val method = clazz.getDeclaredMethod("setApplicationContextV0", Context::class.java)
+ method.isAccessible = true
+ invokeStaticMethod(method, arrayOf(context))
+ Log.d(TAG, "Module context set for $targetClassName")
+ } catch (e: NoSuchMethodException) {
+ Log.d(TAG, "No setApplicationContextV0 in $targetClassName, skipping")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to set module context for $moduleName: $targetClassName", e)
+ throw e
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/AppContext.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/AppContext.kt
new file mode 100644
index 0000000000..a36e48758f
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/AppContext.kt
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.common.app
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.content.Context
+
+@SuppressLint("StaticFieldLeak")
+object AppContext {
+ private lateinit var context: Context
+ private var isInitialized = false
+
+ fun init(application: Application) {
+ context = application
+ isInitialized = true
+ }
+ fun get(): Context = context
+ fun isInitialized(): Boolean = isInitialized
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/BaseApplicationContext.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/BaseApplicationContext.kt
new file mode 100644
index 0000000000..109a930f48
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/BaseApplicationContext.kt
@@ -0,0 +1,70 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.common.app
+
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.annotation.Keep
+
+@Keep
+abstract class BaseApplicationContext: ContextWrapper {
+ private var baseContext: Context?
+ private var globalGmsState: BaseApplicationContext?
+ private var inSafeBoot = false
+
+ constructor(base: Context?): this(base, null)
+
+ constructor(context: Context?, baseApplicationContext: BaseApplicationContext?) : super(context) {
+ inSafeBoot = false
+ baseContext = context
+ globalGmsState = baseApplicationContext
+ }
+
+ public override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ this.baseContext = base
+ }
+
+ override fun createAttributionContext(attributionTag: String?): Context {
+ return ApplicationContextWrapper(this, super.createAttributionContext(attributionTag))
+ }
+
+ override fun createDeviceProtectedStorageContext(): Context {
+ return ApplicationContextWrapper(this, super.createDeviceProtectedStorageContext())
+ }
+
+ override fun getBaseContext(): Context? {
+ return baseContext
+ }
+
+ protected fun getGlobalState(): BaseApplicationContext? {
+ return this.globalGmsState
+ }
+
+ fun getInSafeBoot(): Boolean {
+ return this.inSafeBoot
+ }
+ fun setInSafeBoot() {
+ this.inSafeBoot = true
+ }
+
+ fun watchForLeaks(object0: Any?) {
+ }
+
+ class ApplicationContextWrapper(private val originalContext: Context, base: Context) :
+ ContextWrapper(base) {
+ override fun getApplicationContext(): Context {
+ return originalContext
+ }
+
+ override fun createAttributionContext(attributionTag: String?): Context {
+ return ApplicationContextWrapper(originalContext, super.createAttributionContext(attributionTag))
+ }
+
+ override fun createDeviceProtectedStorageContext(): Context {
+ return ApplicationContextWrapper(originalContext, super.createDeviceProtectedStorageContext())
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/GCoreApplicationContext.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/GCoreApplicationContext.kt
new file mode 100644
index 0000000000..134568376a
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/common/app/GCoreApplicationContext.kt
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.common.app
+
+class GCoreApplicationContext private constructor() : BaseApplicationContext(null) {
+
+ companion object {
+ val instance: GCoreApplicationContext by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
+ GCoreApplicationContext()
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/common/threads/internal/GlobalExecutorsImpl.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/common/threads/internal/GlobalExecutorsImpl.kt
new file mode 100644
index 0000000000..d9e4f5e17e
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/common/threads/internal/GlobalExecutorsImpl.kt
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.common.threads.internal
+
+import androidx.annotation.Keep
+import java.util.concurrent.SynchronousQueue
+import java.util.concurrent.ThreadFactory
+import java.util.concurrent.ThreadPoolExecutor
+import java.util.concurrent.TimeUnit
+
+@Keep
+object GlobalExecutorsImpl {
+ private val lowPool = ThreadPoolExecutor(
+ 4, Int.MAX_VALUE, 10L, TimeUnit.SECONDS, SynchronousQueue(),
+ NamedThreadFactory("lowpool", Thread.MAX_PRIORITY)
+ )
+
+ private val highPool = ThreadPoolExecutor(
+ 4, Int.MAX_VALUE, 10L, TimeUnit.SECONDS, SynchronousQueue(),
+ NamedThreadFactory("highpool", Thread.NORM_PRIORITY)
+ )
+
+ private val activePool = ThreadPoolExecutor(
+ Runtime.getRuntime().availableProcessors(), Int.MAX_VALUE, 10L, TimeUnit.SECONDS, SynchronousQueue(),
+ NamedThreadFactory("actvpool", Thread.MIN_PRIORITY)
+ )
+
+ @Keep
+ @JvmStatic
+ fun getPool(priority: Int): ThreadPoolExecutor {
+ return when (priority) {
+ 0 -> activePool
+ 9 -> highPool
+ 10 -> lowPool
+ else -> throw IllegalArgumentException("Unexpected priority $priority")
+ }
+ }
+
+ private class NamedThreadFactory(private val namePrefix: String, private val priority: Int) :
+ ThreadFactory {
+ private var count = 0
+
+ override fun newThread(r: Runnable): Thread {
+ val t = Thread(r, namePrefix + "-" + (++count))
+ try {
+ t.priority = priority
+ } catch (ignored: Exception) {
+ }
+ return t
+ }
+ }
+}
diff --git a/play-services-chimera-core/src/main/java/com/google/android/gms/mlkit/docscan/ui/DocumentScanningActivity.kt b/play-services-chimera-core/src/main/java/com/google/android/gms/mlkit/docscan/ui/DocumentScanningActivity.kt
new file mode 100644
index 0000000000..189e5c89eb
--- /dev/null
+++ b/play-services-chimera-core/src/main/java/com/google/android/gms/mlkit/docscan/ui/DocumentScanningActivity.kt
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.google.android.gms.mlkit.docscan.ui
+
+import androidx.annotation.Keep
+import com.google.android.chimera.android.ChimeraActivityProxy
+
+@Keep
+class DocumentScanningActivity: ChimeraActivityProxy()
diff --git a/play-services-chimera-core/src/main/res/values-zh-rCN/strings.xml b/play-services-chimera-core/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..85a0f70013
--- /dev/null
+++ b/play-services-chimera-core/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,34 @@
+
+
+ 需要模块
+ 此功能需要额外模块。点按下载后,将由外部应用下载模块。下载完成后,请使用 MG 服务打开 .mods 文件并完成验证和导入。\n\n请求的功能:%1$s
+ 下载
+ 选择下载应用
+ 取消
+ 文档扫描
+ %1$s模块
+ 下载说明
+ %1$s功能需要额外模块。MG 服务将打开外部应用进行下载。下载完成后,请使用 MG 服务打开 .mods 文件并完成验证和导入。
+ 所需权限
+ • %1$s — %2$s(%3$s)
+ 已授权
+ 未授权
+ 无需额外权限。
+ 下载前必须授予全部所需权限。授权完成后,MG 服务将打开外部下载应用。
+ 授权并下载
+ 打开权限设置
+ 尚未授予所需权限,当前无法进入下载页面。
+ 无法打开应用权限设置。
+ 没有可用于下载此模块的外部应用。
+ 相机
+ 允许 MG 服务拍摄待扫描的文档页面。
+ 需要重新授权
+ %1$s模块已安装,但所需权限已被关闭。重新授权后才能继续使用此功能。
+ 使用此模块前必须授予全部所需权限。授权完成后将自动重新打开请求的功能。
+ 授权并继续
+ 继续
+ 尚未授予所需权限,当前无法使用此模块。
+ 无法重新打开请求的模块。
+ 正在等待模块下载并由 MG 服务打开。导入成功后,将自动继续当前请求。
+ 重新打开下载
+
diff --git a/play-services-chimera-core/src/main/res/values-zh-rTW/strings.xml b/play-services-chimera-core/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..215ccf850b
--- /dev/null
+++ b/play-services-chimera-core/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,34 @@
+
+
+ 需要模組
+ 此功能需要額外模組。點按下載後,將由外部應用程式下載模組。下載完成後,請使用 MG 服務開啟 .mods 檔案並完成驗證和匯入。\n\n要求的功能:%1$s
+ 下載
+ 選擇下載應用程式
+ 取消
+ 文件掃描
+ %1$s模組
+ 下載說明
+ %1$s功能需要額外模組。MG 服務將開啟外部應用程式進行下載。下載完成後,請使用 MG 服務開啟 .mods 檔案並完成驗證和匯入。
+ 所需權限
+ • %1$s — %2$s(%3$s)
+ 已授權
+ 未授權
+ 不需要額外權限。
+ 下載前必須授予全部所需權限。授權完成後,MG 服務將開啟外部下載應用程式。
+ 授權並下載
+ 開啟權限設定
+ 尚未授予所需權限,目前無法進入下載頁面。
+ 無法開啟應用程式權限設定。
+ 沒有可用於下載此模組的外部應用程式。
+ 相機
+ 允許 MG 服務拍攝要掃描的文件頁面。
+ 需要重新授權
+ %1$s模組已安裝,但所需權限已被關閉。重新授權後才能繼續使用此功能。
+ 使用此模組前必須授予全部所需權限。授權完成後將自動重新開啟要求的功能。
+ 授權並繼續
+ 繼續
+ 尚未授予所需權限,目前無法使用此模組。
+ 無法重新開啟要求的模組。
+ 正在等待模組下載並由 MG 服務開啟。匯入成功後,將自動繼續目前的要求。
+ 重新開啟下載
+
diff --git a/play-services-chimera-core/src/main/res/values/strings.xml b/play-services-chimera-core/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..0f63698ae3
--- /dev/null
+++ b/play-services-chimera-core/src/main/res/values/strings.xml
@@ -0,0 +1,38 @@
+
+
+
+ Module required
+ This feature needs an extra module. Tap Download to open it in an external download app. After downloading, open the .mods file with microG to verify and import it.\n\nRequested features: %1$s
+ Download
+ Choose a download application
+ Cancel
+ Document scanner
+ %1$s module
+ Download
+ The %1$s feature requires an additional module. microG will open an external application to download it. Open the downloaded .mods file with microG to verify and import it.
+ Required permissions
+ • %1$s — %2$s (%3$s)
+ Granted
+ Required
+ No additional permissions are required.
+ All required permissions must be granted before downloading. After authorization, microG will open an external download application.
+ Authorize and download
+ Open permission settings
+ Permission was not granted. Download remains unavailable.
+ Unable to open the application permission settings.
+ No external application is available to download this module.
+ Camera
+ Allows microG services to capture document pages for scanning.
+ Permission required
+ The %1$s module is installed, but a required permission has been revoked. Grant it again to continue using this feature.
+ All required permissions must be granted before using this module. After authorization, the requested feature opens automatically.
+ Authorize and continue
+ Continue
+ Permission was not granted. This module cannot be used.
+ Unable to reopen the requested module.
+ Waiting for the module to be downloaded and opened with microG. This request will continue automatically after a successful import.
+ Open download again
+
diff --git a/play-services-chimeraresources/build.gradle b/play-services-chimeraresources/build.gradle
new file mode 100644
index 0000000000..3ff82db139
--- /dev/null
+++ b/play-services-chimeraresources/build.gradle
@@ -0,0 +1,22 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ namespace 'com.google.android.chimeraresources'
+ compileSdkVersion androidCompileSdk
+
+ defaultConfig {
+ versionName version
+ minSdkVersion androidMinSdk
+ targetSdkVersion androidTargetSdk
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+}
diff --git a/play-services-chimeraresources/src/main/res/values/ids.xml b/play-services-chimeraresources/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..8648ddf518
--- /dev/null
+++ b/play-services-chimeraresources/src/main/res/values/ids.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/play-services-core-proto/src/main/proto/chimera/chimeramanifest.proto b/play-services-core-proto/src/main/proto/chimera/chimeramanifest.proto
new file mode 100644
index 0000000000..a8e274f490
--- /dev/null
+++ b/play-services-core-proto/src/main/proto/chimera/chimeramanifest.proto
@@ -0,0 +1,80 @@
+syntax = "proto2";
+
+package com.google.android.chimera.config;
+
+option java_multiple_files = true;
+option java_outer_classname = "ChimeraManifest";
+
+message ChimeraManifestStore {
+ repeated ChimeraModule chimeraModules = 1;
+ optional ChimeraModuleCollections collections = 2;
+ repeated FeatureDescriptor featureDescriptors = 3;
+}
+
+message ChimeraModule {
+ optional int32 type = 1;
+ optional string installedApkPath = 2;
+ optional int64 timestamp = 3;
+ optional string moduleApiClassname = 4;
+ optional string packageName = 5;
+ optional string versionName = 6;
+ optional int32 versionCode = 7;
+ optional int32 sourceType = 8;
+ repeated DependApkEntry dependencyModules = 9;
+ optional int32 required = 10;
+ optional int32 moduleType = 11;
+ optional string sourceUri = 12;
+ optional string moduleName = 13;
+ optional string moduleVersion = 14;
+ optional int32 moduleStatus = 16;
+ optional int32 splitDeliveryType = 17;
+ optional string moduleId = 20;
+ optional string apkSha256 = 21;
+}
+
+message DependApkEntry {
+ optional string packageName = 1;
+ optional string moduleName = 2;
+ optional string version = 3;
+}
+
+message ChimeraModuleCollections {
+ optional string chimeraClassNamePrefix = 4;
+ repeated ComponentRoute activeRoutes = 1;
+ repeated ComponentRoute boundServiceRoutes = 2;
+ repeated ComponentRoute serviceRoutes = 3;
+ repeated ComponentRoute providerRoutes = 5;
+ repeated ComponentRoute receiverRoutes = 6;
+}
+
+message ComponentRoute {
+ optional string containerName = 1;
+ optional string moduleChimeraName = 2;
+ optional string moduleId = 3;
+}
+
+message ChimeraManifest {
+ repeated ChimeraModuleManifest chimeraModuleManifests = 1;
+}
+
+message ChimeraModuleManifest {
+ optional string moduleId = 1;
+ optional int32 moduleVersion = 2;
+ optional string requiredApis = 3;
+ repeated ComponentBinding providerBindings = 8;
+ repeated ComponentBinding activityBindings = 10;
+ optional string chimeraClassNamePrefix = 12;
+ repeated ComponentBinding boundServiceBindings = 13;
+ repeated FeatureDescriptor featureDescriptors = 15;
+ repeated ComponentBinding sliceProviderBindings = 18;
+}
+
+message ComponentBinding {
+ optional string containerName = 1;
+ optional string moduleChimeraName = 2;
+}
+
+message FeatureDescriptor {
+ optional string featureName = 1;
+ optional int64 featureVersion = 2;
+}
diff --git a/play-services-core-proto/src/main/proto/chimera/modulefile.proto b/play-services-core-proto/src/main/proto/chimera/modulefile.proto
new file mode 100644
index 0000000000..13bef0f3eb
--- /dev/null
+++ b/play-services-core-proto/src/main/proto/chimera/modulefile.proto
@@ -0,0 +1,14 @@
+syntax = "proto2";
+
+option java_package = "com.google.android.gms.chimera";
+option java_multiple_files = false;
+
+message ModuleInfo {
+ optional string source = 1;
+ optional int32 status = 2;
+ optional string filename = 3;
+ optional string sha256_hash = 4;
+ optional int32 priority = 5;
+ optional string module_name = 6;
+ optional string module_version = 7;
+}
diff --git a/play-services-core-proto/src/main/proto/chimera/modulemanager.proto b/play-services-core-proto/src/main/proto/chimera/modulemanager.proto
new file mode 100644
index 0000000000..e8888e2d2d
--- /dev/null
+++ b/play-services-core-proto/src/main/proto/chimera/modulemanager.proto
@@ -0,0 +1,20 @@
+syntax = "proto2";
+
+package com.google.android.chimera.config;
+import "chimera/chimeramanifest.proto";
+
+option java_multiple_files = true;
+option java_outer_classname = "ModuleManagerProto";
+
+message FeatureMessage {
+ optional string featureName = 1;
+ optional int64 featureVersion = 2;
+ optional bool unknownBool3 = 3;
+ optional bool unknownBool4 = 4;
+ optional bool unknownBool5 = 5;
+ repeated FeatureDescriptor featureDescriptor = 6;
+}
+
+message FeaturesMessage {
+ repeated FeatureMessage features = 1;
+}
diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle
index 4d25419ecd..16808c5eb8 100644
--- a/play-services-core/build.gradle
+++ b/play-services-core/build.gradle
@@ -52,6 +52,7 @@ dependencies {
implementation project(':play-services-wearable-core')
implementation project(':play-services-core-proto')
+ implementation project(':play-services-chimera-core')
implementation project(':play-services-core:microg-ui-tools') // deprecated
implementation project(':play-services-base-core-package')
diff --git a/play-services-core/src/huawei/AndroidManifest.xml b/play-services-core/src/huawei/AndroidManifest.xml
index ed5b25087c..3b4f76906f 100644
--- a/play-services-core/src/huawei/AndroidManifest.xml
+++ b/play-services-core/src/huawei/AndroidManifest.xml
@@ -58,6 +58,9 @@
+
diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml
index 713deb4842..f5a958f728 100644
--- a/play-services-core/src/main/AndroidManifest.xml
+++ b/play-services-core/src/main/AndroidManifest.xml
@@ -157,6 +157,7 @@
+
@@ -527,6 +528,50 @@
android:process=":ui"
android:theme="@style/Theme.App.Translucent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -806,11 +851,67 @@
android:exported="false" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
sContextCache = new WeakHashMap<>();
- // WeakHashMap cannot be used, and there is a high probability that it will be recycled, causing ClassLoader to be rebuilt
+ private static final Map sContextCache = new WeakHashMap<>();
+ // Must not use WeakHashMap here: entries would likely be reclaimed, forcing the ClassLoader to be rebuilt
private static final Map sClassLoaderCache = new HashMap<>();
- public static DynamiteContext createDynamiteContext(String moduleId, Context originalContext) {
+ public static void clearCacheForModule(String moduleId) {
+ if (moduleId == null || moduleId.isEmpty()) return;
+ String prefix = moduleId + "-";
+ removeCacheEntries(sContextCache, prefix);
+ removeCacheEntries(sClassLoaderCache, prefix);
+ Log.d(TAG, "Cleared Dynamite caches for moduleId: " + moduleId);
+ }
+
+ private static void removeCacheEntries(Map cache, String prefix) {
+ synchronized (cache) {
+ Iterator iterator = cache.keySet().iterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().startsWith(prefix)) iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * The third argument remains for the upstream-compatible call shape. It is deliberately not
+ * trusted: actual module selection and cache identity come from the verified local config.
+ */
+ public static Context createDynamiteContext(String moduleId, Context originalContext, String ignoredLoaderPath) {
+ Log.d(TAG, "create moduleId: " + moduleId);
if (originalContext == null) {
Log.w(TAG, "create Original context is null");
return null;
}
- String cacheKey = moduleId + "-" + originalContext.getPackageName();
- synchronized (sContextCache) {
- DynamiteContext cached = sContextCache.get(cacheKey);
- if (cached != null) {
- Log.d(TAG, "Using cached DynamiteContext for cacheKey: " + cacheKey);
- return cached;
- }
- }
try {
DynamiteModuleInfo moduleInfo = new DynamiteModuleInfo(moduleId);
Context gmsContext = originalContext.createPackageContext(Constants.GMS_PACKAGE_NAME, 0);
Context originalAppContext = originalContext.getApplicationContext();
- DynamiteContext dynamiteContext;
+ Context dynamiteContext;
if (originalAppContext == null || originalAppContext == originalContext) {
dynamiteContext = new DynamiteContext(moduleInfo, originalContext, gmsContext, null);
} else {
dynamiteContext = new DynamiteContext(moduleInfo, originalContext, gmsContext, new DynamiteContext(moduleInfo, originalAppContext, gmsContext, null));
}
moduleInfo.init(dynamiteContext);
+ Log.d(TAG, "init " + moduleInfo.getModuleId());
+ // Module downloads may be written by another process. Refresh cached manifest
+ // before resolving module metadata in this process.
+ try {
+ com.google.android.chimera.config.ChimeraConfigManager.INSTANCE.reload();
+ } catch (Exception ignored) {
+ }
+ String lookupModuleId = moduleInfo.getModuleId();
+ // Prefer the installed module's moduleName/version (from chimera_manifest.pb); fall back to the registry when not installed
+ com.google.android.chimera.config.ChimeraModule installedForCtx =
+ com.google.android.chimera.config.ChimeraConfigManager.INSTANCE.findModuleByModuleId(lookupModuleId);
+ DynamicModuleRegistry.DynamicModule moduleEntry = DynamicModuleRegistry.INSTANCE.getByModuleId(lookupModuleId);
+ // A replacement can reuse the same module ID and version. The persisted digest, rather than a
+ // caller-supplied loader path, therefore separates cache entries across verified artifacts.
+ String cacheIdentity = "builtin";
+ if (installedForCtx != null) {
+ String installedPath = installedForCtx.installedApkPath;
+ if (installedPath == null || ChimeraStorage.INSTANCE.verifiedModuleApk(
+ originalContext, new File(installedPath), null, installedForCtx.apkSha256) == null) {
+ Log.w(TAG, "No verified dynamic module available for " + lookupModuleId);
+ return null;
+ }
+ String installedDigest = installedForCtx.apkSha256;
+ cacheIdentity = installedDigest != null && !installedDigest.isEmpty()
+ ? installedDigest : "unverified";
+ }
+ String cacheKey = lookupModuleId + "-" + originalContext.getPackageName() + "-" + cacheIdentity;
+ synchronized (sContextCache) {
+ Context cached = sContextCache.get(cacheKey);
+ if (cached != null) {
+ Log.d(TAG, "Using cached DynamiteContext for moduleId: " + lookupModuleId);
+ return cached;
+ }
+ }
+
+ if (installedForCtx != null || moduleEntry != null) {
+ String moduleName = (installedForCtx != null && installedForCtx.moduleName != null
+ && !installedForCtx.moduleName.isEmpty())
+ ? installedForCtx.moduleName
+ : (moduleEntry != null ? moduleEntry.getModuleName() : "");
+ int moduleVersion;
+ if (installedForCtx != null && installedForCtx.moduleVersion != null) {
+ int pv = 0;
+ try {
+ pv = Integer.parseInt(installedForCtx.moduleVersion);
+ } catch (NumberFormatException ignored) {
+ }
+ moduleVersion = pv;
+ } else {
+ // installedForCtx is null for a feature sub-moduleId (e.g. mlkit_docscan_detect) that has no
+ // config entry of its own. Inherit the installed parent module's version (resolved by
+ // moduleName) instead of persisting 0 below: a 0 here gets written into the config entry and
+ // then makes every later getModuleVersion2(sub-moduleId) report "not installed", wrongly
+ // triggering the (now removed) install flow and breaking the feature on its next use.
+ com.google.android.chimera.config.ChimeraModule parent = moduleName.isEmpty() ? null
+ : com.google.android.chimera.config.ChimeraConfigManager.INSTANCE.findInstalledModuleByName(moduleName);
+ int pv = 0;
+ if (parent != null && parent.moduleVersion != null) {
+ try {
+ pv = Integer.parseInt(parent.moduleVersion);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ moduleVersion = pv;
+ }
+ // The loader resolves only configuration records whose artifact has passed integrity checks.
+ Context moduleData = ChimeraModuleLdr.INSTANCE.loadModule(
+ dynamiteContext, lookupModuleId, moduleName, moduleVersion);
+ if (moduleData != null) {
+ dynamiteContext = moduleData;
+ Log.d(TAG, "Module loaded via ChimeraModuleLdr: " + moduleInfo.getModuleId());
+ } else {
+ Log.w(TAG, "No verified dynamic module available for " + moduleInfo.getModuleId());
+ }
+ } else {
+ Log.d(TAG, "No DynamicModuleRegistry entry for " + moduleInfo.getModuleId());
+ }
+ Log.d(TAG, "DC createClassLoader " + moduleInfo.getModuleId() + " ClassLoader: " + dynamiteContext.getClassLoader());
synchronized (sContextCache) {
sContextCache.put(cacheKey, dynamiteContext);
}
- Log.d(TAG, "Created and cached a new DynamiteContext for cacheKey: " + cacheKey);
+ Log.d(TAG, "Created and cached a new DynamiteContext for moduleId: " + lookupModuleId);
return dynamiteContext;
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, e);
@@ -92,12 +189,44 @@ public static ClassLoader createClassLoader(DynamiteModuleInfo moduleInfo, Conte
} else {
nativeLoaderDirs.append(File.pathSeparator).append(gmsContext.getApplicationInfo().sourceDir).append("!/lib/").append(CPU_ABI);
}
- ClassLoader classLoader = new PathClassLoader(gmsContext.getApplicationInfo().sourceDir, nativeLoaderDirs.toString(), new FilteredClassLoader(originalContext.getClassLoader(), moduleInfo.getMergedClasses(), moduleInfo.getMergedPackages()));
+ Collection mergedClasses = moduleInfo.getMergedClasses();
+ Collection mergedPackages = moduleInfo.getMergedPackages();
+
+ // Some module descriptors do not publish merge allow-lists. In that case,
+ // using FilteredClassLoader blocks non-boot dependencies (e.g. obfuscated
+ // host classes like m38.*), causing module activity class resolution to fail.
+ ClassLoader parent = originalContext.getClassLoader();
+ if (!mergedClasses.isEmpty() || !mergedPackages.isEmpty()) {
+ parent = new FilteredClassLoader(parent, mergedClasses, mergedPackages);
+ } else {
+ Log.d(TAG, "No merged class/package allow-list for " + moduleInfo.getModuleId() + ", using unfiltered parent ClassLoader");
+ }
+
+ ClassLoader classLoader = createSelfFirstClassLoader(gmsContext.getApplicationInfo().sourceDir, nativeLoaderDirs.toString(), parent);
synchronized (sClassLoaderCache) {
sClassLoaderCache.put(cacheKey, classLoader);
}
Log.d(TAG, "Created and cached a new ClassLoader for cacheKey: " + cacheKey + " ClassLoader: " + classLoader.hashCode());
return classLoader;
}
+
+ // Dynamite modules run inside the *client's* process. With a parent-first PathClassLoader, any class the
+ // client also bundles — notably official @SafeParcelable data classes like GoogleCertificatesLookupQuery or
+ // measurement's InitializationParams/ScionActivityInfo — resolves to the client's official version while the
+ // module's service code is ours, causing ABI crashes (NoSuchFieldError on CREATOR). Loading self-first makes
+ // module-bundled classes (our data classes, present in the GMS APK) load from the module, while host-only
+ // classes (obfuscated m38.* etc., absent from our APK) still fall through to the client. Official GMS doesn't
+ // need this — its module code and the client's data classes come from the same build, ABI-identical regardless
+ // of load order; microG's data classes differ, so explicit self-first isolation is required. This makes every
+ // built-in dynamite service module (googlecertificates, measurement, ...) safe at once, replacing per-module
+ // MERGED_CLASSES workarounds and covering future modules automatically.
+ private static ClassLoader createSelfFirstClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
+ if (SDK_INT >= 27) { // DelegateLastClassLoader (self-first) available since API 27 (Android 8.1)
+ return new dalvik.system.DelegateLastClassLoader(dexPath, librarySearchPath, parent);
+ }
+ // Pre-27 fallback: keep parent-first. Such old devices rarely run client SDKs that bundle the conflicting
+ // official data classes, so the residual risk is negligible.
+ return new PathClassLoader(dexPath, librarySearchPath, parent);
+ }
}
diff --git a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java
index 608cc3c920..e90fcf4aef 100644
--- a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java
+++ b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java
@@ -9,12 +9,15 @@
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.gms.chimera.DynamiteContextFactory;
+import java.io.File;
+
public class DynamiteContext extends ContextWrapper {
- private static final String TAG = "DynamiteContext";
private DynamiteModuleInfo moduleInfo;
private Context originalContext;
private Context gmsContext;
@@ -43,6 +46,22 @@ public String getPackageName() {
return gmsContext.getPackageName();
}
+ @Override
+ public File getDir(String name, int mode) {
+ if ("chimera".equals(name)) {
+ return gmsContext.getDir(name, mode);
+ } else {
+ return super.getDir(name, mode);
+ }
+ }
+
+ @RequiresApi(30)
+ @NonNull
+ @Override
+ public Context createAttributionContext(@Nullable String attributionTag) {
+ return new DynamiteContext(moduleInfo, super.createAttributionContext(attributionTag), gmsContext.createAttributionContext(attributionTag), this);
+ }
+
@Override
public ApplicationInfo getApplicationInfo() {
return gmsContext.getApplicationInfo();
@@ -50,7 +69,7 @@ public ApplicationInfo getApplicationInfo() {
@Override
public Context getApplicationContext() {
- return appContext;
+ return appContext == null ? this : appContext;
}
@RequiresApi(24)
diff --git a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java
index 07be4d28ec..8254c2727f 100644
--- a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java
+++ b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java
@@ -16,59 +16,156 @@
package com.google.android.gms.chimera.container;
+import android.content.ContentProviderClient;
import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
+import com.google.android.chimera.config.ChimeraConfigManager;
+import com.google.android.chimera.config.ChimeraModule;
+import com.google.android.chimera.config.ChimeraStorage;
+import com.google.android.chimera.config.registry.DynamicModuleRegistry;
import com.google.android.gms.chimera.DynamiteContextFactory;
import com.google.android.gms.dynamic.IObjectWrapper;
import com.google.android.gms.dynamic.ObjectWrapper;
import com.google.android.gms.dynamite.IDynamiteLoader;
+import java.io.File;
+
public class DynamiteLoaderImpl extends IDynamiteLoader.Stub {
private static final String TAG = "GmsDynamiteLoaderImpl";
@Override
public IObjectWrapper createModuleContext(IObjectWrapper wrappedContext, String moduleId, int minVersion) throws RemoteException {
- // We don't have crash utils, so just forward
- return createModuleContextV2(wrappedContext, moduleId, minVersion);
+ return createModuleContextNoCrashUtils(wrappedContext, moduleId, minVersion);
}
@Override
- public IObjectWrapper createModuleContextV2(IObjectWrapper wrappedContext, String moduleId, int minVersion) throws RemoteException {
- Log.d(TAG, "createModuleContext for " + moduleId + " at version " + minVersion);
- final Context originalContext = (Context) ObjectWrapper.unwrap(wrappedContext);
- return ObjectWrapper.wrap(DynamiteContextFactory.createDynamiteContext(moduleId, originalContext));
+ public IObjectWrapper createModuleContextNoCrashUtils(IObjectWrapper wrappedContext, String moduleId, int minVersion) throws RemoteException {
+ Log.d(TAG, "createModuleContextNoCrashUtils: " + moduleId + " at version " + minVersion);
+ return createModuleContext3NoCrashUtils(wrappedContext, moduleId, minVersion, null);
}
@Override
- public IObjectWrapper createModuleContextV3(IObjectWrapper wrappedContext, String moduleId, int minVersion, IObjectWrapper wrappedCursor) throws RemoteException {
- throw new UnsupportedOperationException();
+ public IObjectWrapper createModuleContext3NoCrashUtils(IObjectWrapper wrappedContext, String moduleId, int minVersion, IObjectWrapper wrappedCursor) throws RemoteException {
+ Log.d(TAG, "createModuleContext3NoCrashUtils: " + moduleId + " at version " + minVersion);
+ final Context originalContext = (Context) ObjectWrapper.unwrap(wrappedContext);
+ if (originalContext == null) {
+ Log.w(TAG, "Invalid client context");
+ return ObjectWrapper.wrap(null);
+ }
+ if (isUnavailableDynamicModule(originalContext, moduleId)) {
+ Log.d(TAG, "Dynamic module unavailable: " + moduleId);
+ return ObjectWrapper.wrap(null);
+ }
+
+ if (wrappedCursor != null) {
+ android.database.Cursor cursor = (android.database.Cursor) ObjectWrapper.unwrap(wrappedCursor);
+ if (cursor != null && cursor.moveToFirst()) {
+ int availableVersion = cursor.getInt(0);
+ if (availableVersion < minVersion) {
+ Log.e(TAG, "Requested version " + minVersion + " > available " + availableVersion);
+ return ObjectWrapper.wrap(null);
+ }
+ }
+ }
+
+ // Cursor metadata participates in version negotiation only. APK selection is resolved from
+ // ChimeraConfigManager's digest-verified record inside DynamiteContextFactory.
+ return ObjectWrapper.wrap(DynamiteContextFactory.createDynamiteContext(moduleId, originalContext, null));
}
@Override
- public int getIDynamiteLoaderVersion() throws RemoteException {
- return 2;
+ public int getIDynamiteLoaderVersion() {
+ return 3;
}
@Override
- public int getModuleVersion(IObjectWrapper wrappedContext, String moduleId) throws RemoteException {
+ public int getModuleVersion(IObjectWrapper wrappedContext, String moduleId) {
return getModuleVersion2(wrappedContext, moduleId, true);
}
@Override
- public int getModuleVersion2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) throws RemoteException {
- // We don't have crash utils, so just forward
- return getModuleVersionV2(wrappedContext, moduleId, updateConfigIfRequired);
+ public int getModuleVersion2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) {
+ return getModuleVersion2NoCrashUtils(wrappedContext, moduleId, updateConfigIfRequired);
}
@Override
- public int getModuleVersionV2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) throws RemoteException {
+ public int getModuleVersion2NoCrashUtils(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) {
+ Log.d(TAG, "getModuleVersion2NoCrashUtils: " + moduleId + "----" + updateConfigIfRequired);
+
final Context context = (Context) ObjectWrapper.unwrap(wrappedContext);
if (context == null) {
Log.w(TAG, "Invalid client context");
return 0;
}
+ if (isUnavailableDynamicModule(context, moduleId)) {
+ Log.d(TAG, "Dynamic module unavailable: " + moduleId);
+ return 0;
+ }
+
+ // Prefer already-installed modules (dynamic chimera_manifest.pb) and report their actual
+ // version (the real version imported from the bundle) before falling back to remote lookup.
+ ChimeraModule installed = ChimeraConfigManager.INSTANCE.findModuleByModuleId(moduleId);
+ if (installed != null && installed.installedApkPath != null
+ && !installed.installedApkPath.isEmpty()
+ && ChimeraStorage.INSTANCE.verifiedModuleApk(
+ context, new File(installed.installedApkPath), null, installed.apkSha256) != null) {
+ int v = 0;
+ try { v = installed.moduleVersion != null ? Integer.parseInt(installed.moduleVersion) : 0; }
+ catch (NumberFormatException ignored) {}
+ if (v > 0) {
+ Log.d(TAG, "getModuleVersion2: " + moduleId + " installed v" + v + " (chimera config)");
+ return v;
+ }
+ }
+
+ DynamicModuleRegistry.DynamicModule moduleEntry = DynamicModuleRegistry.INSTANCE.getByModuleId(moduleId);
+ if (moduleEntry != null) {
+ // A feature sub-moduleId (e.g. com.google.android.gms.mlkit_docscan_detect) ships inside its parent
+ // module and has no config entry of its own, so the exact findModuleByModuleId above misses it; the
+ // loader may also have persisted a placeholder entry for it with version 0. Resolve the version from
+ // the installed parent module (by moduleName, skipping the version-0 placeholder), matching the
+ // feature-availability path (ChimeraModuleManager). Otherwise an already-present feature is reported
+ // as "not installed" and the caller triggers the (now removed) install flow, breaking it on reuse.
+ ChimeraModule parent = ChimeraConfigManager.INSTANCE.findInstalledModuleByName(moduleEntry.getModuleName());
+ if (parent != null && parent.installedApkPath != null
+ && ChimeraStorage.INSTANCE.verifiedModuleApk(
+ context, new File(parent.installedApkPath), null, parent.apkSha256) != null) {
+ int pv = 0;
+ try { pv = parent.moduleVersion != null ? Integer.parseInt(parent.moduleVersion) : 0; }
+ catch (NumberFormatException ignored) {}
+ if (pv > 0) {
+ Log.d(TAG, "getModuleVersion2NoCrashUtils: " + moduleId + " provided by installed module "
+ + moduleEntry.getModuleName() + " v" + pv);
+ return pv;
+ }
+ }
+
+ // Only report a remote version for dynamic modules that are actually available.
+ // Otherwise callers skip install flow and jump directly into a missing-resource page.
+ Cursor availability = null;
+ try {
+ availability = queryForDynamiteModule(context, moduleId, updateConfigIfRequired);
+ if (availability != null && availability.moveToFirst() && availability.getInt(0) > 0) {
+ int availableVersion = availability.getInt(0);
+ Log.d(TAG, "getModuleVersion2NoCrashUtils: " + moduleId + " available at version " + availableVersion);
+ return availableVersion;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to query module availability for " + moduleId, e);
+ } finally {
+ if (availability != null) {
+ availability.close();
+ }
+ }
+ Log.d(TAG, "getModuleVersion2NoCrashUtils: " + moduleId + " not installed yet, return 0 to trigger install");
+ return 0;
+ }
+ Log.w(TAG, "Failed to retrieve remote feature version.");
try {
return Class.forName("com.google.android.gms.dynamite.descriptors." + moduleId + ".ModuleDescriptor").getDeclaredField("MODULE_VERSION").getInt(null);
@@ -76,29 +173,136 @@ public int getModuleVersionV2(IObjectWrapper wrappedContext, String moduleId, bo
Log.w(TAG, "No such module known: " + moduleId);
}
- if (moduleId.equals("com.google.android.gms.firebase_database")) {
- Log.d(TAG, "returning temp fix module version for " + moduleId + ". Firebase Database will not be functional!");
- return com.google.android.gms.dynamite.descriptors.com.google.android.gms.firebase_database.ModuleDescriptor.MODULE_VERSION;
+ switch (moduleId) {
+ case "com.google.android.gms.cast.framework.dynamite":
+ Log.d(TAG, "returning temp fix module version for " + moduleId + ". Cast API wil not be functional!");
+ return 1;
+ case "com.google.android.gms.maps_dynamite":
+ Log.d(TAG, "returning v1 for maps");
+ return 1;
}
- if (moduleId.equals("com.google.android.gms.googlecertificates")) {
- return com.google.android.gms.dynamite.descriptors.com.google.android.gms.googlecertificates.ModuleDescriptor.MODULE_VERSION;
+
+ Log.d(TAG, "unimplemented Method: getModuleVersion for " + moduleId);
+ return 0;
+ }
+
+ @Override
+ public IObjectWrapper queryForDynamiteModuleNoCrashUtils(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired, long requestStartTime) throws RemoteException {
+ final Context context = (Context) ObjectWrapper.unwrap(wrappedContext);
+ if (context == null) {
+ Log.w(TAG, "Invalid client Context.");
+ return ObjectWrapper.wrap(null);
}
- if (moduleId.equals("com.google.android.gms.cast.framework.dynamite")) {
- Log.d(TAG, "returning temp fix module version for " + moduleId + ". Cast API wil not be functional!");
- return 1;
+ try {
+ Cursor cursor = queryForDynamiteModule(context, moduleId, updateConfigIfRequired);
+
+ // If ServiceProvider returned a valid cursor with version > 0, use it (Chimera module)
+ if (cursor != null && cursor.moveToFirst() && cursor.getInt(0) > 0) {
+ cursor.moveToPosition(-1); // reset position for caller
+ return ObjectWrapper.wrap(cursor);
+ }
+
+ if (ChimeraConfigManager.INSTANCE.findModuleByModuleId(moduleId) != null
+ || DynamicModuleRegistry.INSTANCE.getByModuleId(moduleId) != null) {
+ if (cursor != null) cursor.close();
+ return ObjectWrapper.wrap(null);
+ }
+
+ // Fallback: build a cursor from V2 version lookup (non-Chimera modules like cast, maps, etc.)
+ int version = getModuleVersion2NoCrashUtils(wrappedContext, moduleId, updateConfigIfRequired);
+ if (version > 0) {
+ Log.d(TAG, "queryForDynamiteModule fallback for " + moduleId + " v" + version);
+ String[] columns = {"version", "apkPath", "loaderPath"};
+ MatrixCursor fallbackCursor = new MatrixCursor(columns, 1);
+ fallbackCursor.addRow(new Object[]{version, null, null});
+ return ObjectWrapper.wrap(fallbackCursor);
+ }
+
+ return ObjectWrapper.wrap(cursor);
+ } catch (Exception e) {
+ Log.e(TAG, "Error retrieving remote feature version: ", e);
+ return ObjectWrapper.wrap(null);
}
+ }
+
+ private Cursor queryForDynamiteModule(Context context, String moduleId, boolean updateConfigIfRequired) {
+ Uri uri = new Uri.Builder()
+ .scheme("content")
+ .authority("com.google.android.gms.chimera")
+ .path(updateConfigIfRequired ? "api_force_staging" : "api")
+ .appendPath(moduleId)
+ .appendQueryParameter("requestStartUptime", "0")
+ .build();
- if (moduleId.equals("com.google.android.gms.maps_dynamite")) {
- Log.d(TAG, "returning v1 for maps");
- return 1;
+ ContentProviderClient contentProviderClient = context.getContentResolver().acquireUnstableContentProviderClient(uri);
+ if (contentProviderClient == null) {
+ return null;
}
- Log.d(TAG, "unimplemented Method: getModuleVersion for " + moduleId);
- return 0;
+ try {
+ Cursor cursor = contentProviderClient.query(uri, null, null, null, null);
+ if (cursor == null) {
+ return null;
+ }
+
+ try {
+ MatrixCursor matrixCursor = new MatrixCursor(cursor.getColumnNames(), cursor.getCount());
+
+ while (cursor.moveToNext()) {
+ Object[] values = new Object[cursor.getColumnCount()];
+ for (int j = 0; j < cursor.getColumnCount(); j++) {
+ switch (cursor.getType(j)) {
+ case Cursor.FIELD_TYPE_NULL:
+ values[j] = null;
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ values[j] = cursor.getLong(j);
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ values[j] = cursor.getDouble(j);
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ values[j] = cursor.getString(j);
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ values[j] = cursor.getBlob(j);
+ break;
+ }
+ }
+ matrixCursor.addRow(values);
+ }
+
+ return matrixCursor;
+ } finally {
+ cursor.close();
+ }
+
+ } catch (RemoteException ignored) {
+ } finally {
+ contentProviderClient.close();
+ }
+ return null;
}
- @Override
- public IObjectWrapper getModuleVersionV3(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired, long requestStartTime) throws RemoteException {
- throw new UnsupportedOperationException();
+ private boolean isUnavailableDynamicModule(Context context, String moduleId) {
+ boolean isDynamicModule = ChimeraConfigManager.INSTANCE.findModuleByModuleId(moduleId) != null
+ || DynamicModuleRegistry.INSTANCE.getByModuleId(moduleId) != null;
+ if (!isDynamicModule) {
+ return false;
+ }
+
+ Cursor availability = null;
+ try {
+ availability = queryForDynamiteModule(context, moduleId, false);
+ return availability == null || !availability.moveToFirst() || availability.getInt(0) <= 0;
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to verify dynamic module availability: " + moduleId, e);
+ return true;
+ } finally {
+ if (availability != null) {
+ availability.close();
+ }
+ }
}
+
}
diff --git a/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_detect/ModuleDescriptor.java b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_detect/ModuleDescriptor.java
new file mode 100644
index 0000000000..328eb1da70
--- /dev/null
+++ b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_detect/ModuleDescriptor.java
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.dynamite.descriptors.com.google.android.gms.mlkit_docscan_detect;
+
+import androidx.annotation.Keep;
+
+@Keep
+public class ModuleDescriptor {
+ public static final String MODULE_ID = "com.google.android.gms.mlkit_docscan_detect";
+ public static final int MODULE_VERSION = 1;
+}
diff --git a/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_enhance/ModuleDescriptor.java b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_enhance/ModuleDescriptor.java
new file mode 100644
index 0000000000..291ffd8569
--- /dev/null
+++ b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_enhance/ModuleDescriptor.java
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.dynamite.descriptors.com.google.android.gms.mlkit_docscan_enhance;
+
+import androidx.annotation.Keep;
+
+@Keep
+public class ModuleDescriptor {
+ public static final String MODULE_ID = "com.google.android.gms.mlkit_docscan_enhance";
+ public static final int MODULE_VERSION = 1;
+}
diff --git a/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_stain/ModuleDescriptor.java b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_stain/ModuleDescriptor.java
new file mode 100644
index 0000000000..90a3b26ce9
--- /dev/null
+++ b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_stain/ModuleDescriptor.java
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.dynamite.descriptors.com.google.android.gms.mlkit_docscan_stain;
+
+import androidx.annotation.Keep;
+
+@Keep
+public class ModuleDescriptor {
+ public static final String MODULE_ID = "com.google.android.gms.mlkit_docscan_stain";
+ public static final int MODULE_VERSION = 1;
+}
diff --git a/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_ui/ModuleDescriptor.java b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_ui/ModuleDescriptor.java
new file mode 100644
index 0000000000..8a3e0cd74d
--- /dev/null
+++ b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_ui/ModuleDescriptor.java
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.dynamite.descriptors.com.google.android.gms.mlkit_docscan_ui;
+
+import androidx.annotation.Keep;
+
+@Keep
+public class ModuleDescriptor {
+ public static final String MODULE_ID = "com.google.android.gms.mlkit_docscan_ui";
+ public static final int MODULE_VERSION = 1;
+ public static final String MODULE_NAME = "mlkit.docscan.ui";
+}
diff --git a/play-services-core/src/main/java/org/microg/gms/ui/MainSettingsActivity.java b/play-services-core/src/main/java/org/microg/gms/ui/MainSettingsActivity.java
index 9ad6108a35..f656c44000 100644
--- a/play-services-core/src/main/java/org/microg/gms/ui/MainSettingsActivity.java
+++ b/play-services-core/src/main/java/org/microg/gms/ui/MainSettingsActivity.java
@@ -23,6 +23,8 @@
import static org.microg.gms.ui.settings.SettingsProviderKt.getAllSettingsProviders;
public class MainSettingsActivity extends AppCompatActivity {
+ public static final String EXTRA_OPEN_DYNAMIC_MODULE_MANAGER = "org.microg.gms.ui.OPEN_DYNAMIC_MODULE_MANAGER";
+ private static final String ACTION_REQUEST_FEATURES_WITH_UI = "com.google.android.chimera.container.REQUEST_FEATURES_WITH_UI";
private AppBarConfiguration appBarConfiguration;
private static final String FIRST_RUN_MASTER = "org.microg.gms_firstRun";
@@ -32,6 +34,19 @@ private NavController getNavController() {
return ((NavHostFragment)getSupportFragmentManager().findFragmentById(R.id.navhost)).getNavController();
}
+ private void openDynamicModuleSettingsIfRequested(Intent intent) {
+ if (intent == null) return;
+ boolean openDynamicModuleSettings = intent.getBooleanExtra(EXTRA_OPEN_DYNAMIC_MODULE_MANAGER, false)
+ || ACTION_REQUEST_FEATURES_WITH_UI.equals(intent.getAction())
+ || (intent.getData() != null
+ && "x-gms-settings".equals(intent.getData().getScheme())
+ && "dynamicmodule".equals(intent.getData().getHost()));
+ if (openDynamicModuleSettings && getNavController().getCurrentDestination() != null
+ && getNavController().getCurrentDestination().getId() != R.id.dynamicModuleManagerFragment) {
+ getNavController().navigate(R.id.dynamicModuleManagerFragment);
+ }
+ }
+
private void showDialogIfNeeded() {
SharedPreferences prefs = getSharedPreferences(FIRST_RUN_MASTER, MODE_PRIVATE);
if (BuildConfig.APPLICATION_ID == Constants.USER_MICROG_PACKAGE_NAME &&
@@ -68,9 +83,17 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
appBarConfiguration = new AppBarConfiguration.Builder(getNavController().getGraph()).build();
NavigationUI.setupWithNavController(toolbarLayout, toolbar, getNavController(), appBarConfiguration);
+ openDynamicModuleSettingsIfRequested(intent);
showDialogIfNeeded();
}
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ openDynamicModuleSettingsIfRequested(intent);
+ }
+
@Override
public boolean onSupportNavigateUp() {
return NavigationUI.navigateUp(getNavController(), appBarConfiguration) || super.onSupportNavigateUp();
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/chimera/ChimeraModuleRemover.kt b/play-services-core/src/main/kotlin/org/microg/gms/chimera/ChimeraModuleRemover.kt
new file mode 100644
index 0000000000..74ee4c3e01
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/chimera/ChimeraModuleRemover.kt
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.microg.gms.chimera
+
+import android.content.Context
+import android.util.Log
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.ChimeraStorage
+import com.google.android.gms.common.app.AppContext
+import org.microg.gms.moduleinstall.ModuleInstaller
+import java.io.File
+
+object ChimeraModuleRemover {
+ private const val TAG = "ChimeraModuleRemover"
+
+ fun remove(context: Context, moduleName: String): Boolean {
+ // The content provider process may not have AppContext initialized; without this, the
+ // saveToFile in updateConfig below silently fails because configFile cannot be resolved
+ // (the removal never reaches chimera_manifest.pb and the module is not deleted).
+ if (!AppContext.isInitialized()) {
+ (context.applicationContext as? android.app.Application)?.let { AppContext.init(it) }
+ }
+ runCatching { ChimeraConfigManager.reload() }
+
+ val configEntry = ChimeraConfigManager.findModuleByModuleName(moduleName)
+ val configPath = configEntry?.installedApkPath
+ // Clean config metadata before deleting the apk so we can still read the old ChimeraManifest.pb and
+ // remove feature descriptors/routes that belonged to this moduleName. Deleting only chimeraModules leaves
+ // stale features behind and makes later availability checks report removed modules as installed.
+ val removedMetadata = ChimeraConfigManager.removeModuleMetadata(moduleName, configPath)
+ if (!removedMetadata.persisted) {
+ Log.e(TAG, "Refusing to delete APK after metadata persistence failed for $moduleName")
+ return false
+ }
+ var deleteFailed = false
+ for (path in (removedMetadata.apkPaths + listOfNotNull(configPath)).distinct()) {
+ if (ChimeraConfigManager.isApkPathReferenced(path)) continue
+ val f = File(path)
+ if (f.exists() && !ChimeraStorage.safeDeleteModuleApk(context, f)) {
+ Log.w(TAG, "Failed to delete module APK")
+ deleteFailed = true
+ }
+ }
+
+ while (true) {
+ val f = ChimeraStorage.findDownloadedApk(context, moduleName) ?: break
+ if (ChimeraConfigManager.isApkPathReferenced(f.absolutePath)) break
+ if (!ChimeraStorage.safeDeleteModuleApk(context, f)) {
+ Log.w(TAG, "Failed to delete orphan APK")
+ deleteFailed = true
+ break
+ }
+ }
+
+ val idsToInvalidate = removedMetadata.moduleIds.ifEmpty { setOfNotNull(configEntry?.moduleId) }
+ if (idsToInvalidate.isEmpty()) {
+ ModuleInstaller.invalidateRuntimeCaches(moduleName = moduleName, apkPath = configPath)
+ } else {
+ idsToInvalidate.forEach { moduleId ->
+ ModuleInstaller.invalidateRuntimeCaches(moduleId, moduleName, configPath)
+ }
+ }
+ return !deleteFailed
+ }
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/chimera/ServiceProvider.kt b/play-services-core/src/main/kotlin/org/microg/gms/chimera/ServiceProvider.kt
index a2cd5e40c1..00df221aac 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/chimera/ServiceProvider.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/chimera/ServiceProvider.kt
@@ -7,17 +7,29 @@ package org.microg.gms.chimera
import android.content.ContentProvider
import android.content.ContentValues
-import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
+import android.os.Binder
import android.os.Bundle
+import android.os.ParcelFileDescriptor
+import android.os.Process
import android.util.Log
import androidx.core.os.bundleOf
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.ChimeraModule
+import com.google.android.chimera.config.ChimeraStorage
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.config.FeatureCheckUtils
+import com.google.android.chimera.config.FeatureMessage
+import com.google.android.chimera.config.FeaturesMessage
+import com.google.android.chimera.config.ModuleManager
+import com.google.android.chimera.config.ModuleDownloadRegistry
import org.microg.gms.DummyService
import org.microg.gms.common.GmsService
-import org.microg.gms.common.RemoteListenerProxy
+import java.io.File
+import java.io.FileNotFoundException
class ServiceProvider : ContentProvider() {
@@ -28,6 +40,8 @@ class ServiceProvider : ContentProvider() {
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
when (method) {
+ "featureCheckCall" -> return featureCheckCall(extras)
+ "featureFetchCall" -> return featureFetchCall(extras)
"serviceIntentCall" -> {
val serviceAction = extras?.getString("serviceActionBundleKey") ?: return null
val context = context!!
@@ -49,6 +63,17 @@ class ServiceProvider : ContentProvider() {
"serviceResponseIntentKey" to intent
)
}
+ "removeModule" -> {
+ val moduleName = arg ?: return null
+ val ctx = context ?: return null
+ val callingUid = Binder.getCallingUid()
+ if (callingUid != Process.myUid()) {
+ Log.w(TAG, "removeModule rejected from uid=$callingUid")
+ return bundleOf("removed" to false)
+ }
+ val ok = ChimeraModuleRemover.remove(ctx, moduleName)
+ return bundleOf("removed" to ok)
+ }
else -> {
Log.d(TAG, "$method: $arg, $extras")
return super.call(method, arg, extras)
@@ -56,10 +81,133 @@ class ServiceProvider : ContentProvider() {
}
}
- override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
- val cursor = MatrixCursor(COLUMNS)
+ override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor {
Log.d(TAG, "query: $uri")
- return cursor
+ try {
+ val ctx = context ?: return MatrixCursor(COLUMNS)
+ if (!DynamicModuleSettings.isAvailable(ctx)) return MatrixCursor(COLUMNS)
+ if (!isExpectedProviderUri(uri)) {
+ Log.w(TAG, "query: rejected unexpected authority=${uri.authority}")
+ return MatrixCursor(COLUMNS)
+ }
+ runCatching { ChimeraConfigManager.reload() }
+ val configLastModified = ChimeraConfigManager.getConfigLastModified(ctx)
+ val pathSegments = uri.pathSegments
+ if (pathSegments.size == 2 && (pathSegments[0] == "api" || pathSegments[0] == "api_force_staging")) {
+ val moduleId = pathSegments[1]
+ if (!isValidModuleId(moduleId)) {
+ Log.w(TAG, "query: rejected invalid moduleId=$moduleId")
+ return MatrixCursor(COLUMNS)
+ }
+ // First check installed modules (dynamic chimera_manifest.pb, actual version/path)
+ val chimeraModule = ChimeraConfigManager.findModuleByModuleId(moduleId)
+ val installedFile = resolveInstalledApkFile(chimeraModule)
+ if (chimeraModule != null && installedFile != null) {
+ val version = chimeraModule.moduleVersion?.toIntOrNull() ?: 1
+ return buildModuleQueryCursor(
+ version,
+ installedFile.absolutePath,
+ configLastModified,
+ chimeraModule.apkSha256.orEmpty()
+ )
+ }
+ Log.w(TAG, "query: module not available: $moduleId")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "query: error processing module query for $uri", e)
+ }
+ return MatrixCursor(COLUMNS)
+ }
+
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
+ val ctx = context ?: rejectOpenFile(uri, "missing context")
+ if (!isExpectedProviderUri(uri)) {
+ rejectOpenFile(uri, "unexpected authority=${uri.authority}")
+ }
+ if (mode != "r") {
+ rejectOpenFile(uri, "non-readonly mode=$mode")
+ }
+ if (!DynamicModuleSettings.isAvailable(ctx)) {
+ rejectOpenFile(uri, "dynamic modules unavailable")
+ }
+ runCatching { ChimeraConfigManager.reload() }
+ val pathSegments = uri.pathSegments
+ if (pathSegments.size == 2 && (pathSegments[0] == "api" || pathSegments[0] == "api_force_staging") &&
+ pathSegments[1].all { it.isDigit() }) {
+ val configFile = ChimeraConfigManager.getConfigFile(ctx)
+ if (!configFile.isFile || !configFile.canRead()) rejectOpenFile(uri, "config file not available")
+ Log.d(TAG, "openFile: serving Chimera config ${configFile.absolutePath}")
+ return ParcelFileDescriptor.open(configFile, ParcelFileDescriptor.MODE_READ_ONLY)
+ }
+ if (pathSegments.size != 2 || pathSegments[0] != "module_apk") {
+ rejectOpenFile(uri, "invalid path")
+ }
+ val moduleId = pathSegments[1]
+ if (!isValidModuleId(moduleId)) {
+ rejectOpenFile(uri, "invalid moduleId=$moduleId")
+ }
+ Log.d(TAG, "openFile: serving APK for moduleId=$moduleId")
+
+ val apkFile = findModuleApkFile(moduleId)
+ if (apkFile != null) {
+ Log.d(TAG, "openFile: returning FD for ${apkFile.absolutePath}")
+ return ParcelFileDescriptor.open(apkFile, ParcelFileDescriptor.MODE_READ_ONLY)
+ }
+ rejectOpenFile(uri, "APK not found for moduleId=$moduleId")
+ }
+
+ private fun featureCheckCall(extras: Bundle?): Bundle {
+ val out = Bundle()
+ val bytes = extras?.getByteArray("featuresBundleKey")
+ if (bytes == null) {
+ Log.e(TAG, "featureCheckCall: missing featuresBundleKey")
+ out.putInt("featuresResult", ModuleManager.FEATURE_CHECK_ERROR)
+ return out
+ }
+ val request = try {
+ FeaturesMessage.ADAPTER.decode(bytes)
+ } catch (e: Exception) {
+ Log.e(TAG, "featureCheckCall: malformed feature request", e)
+ out.putInt("featuresResult", ModuleManager.FEATURE_CHECK_ERROR)
+ return out
+ }
+ runCatching { ChimeraConfigManager.reload() }
+ out.putInt(
+ "featuresResult",
+ FeatureCheckUtils.checkFeatureMessages(
+ request.features,
+ allowStaticRegistry = false,
+ allowDynamicModules = context?.let(DynamicModuleSettings::isAvailable) == true
+ )
+ )
+ return out
+ }
+
+ private fun featureFetchCall(extras: Bundle?): Bundle {
+ val out = Bundle()
+ val names = extras?.getStringArray("featureNamesBundleKey")
+ if (names.isNullOrEmpty()) {
+ Log.e(TAG, "featureFetchCall: missing featureNamesBundleKey")
+ out.putInt("featuresResult", ModuleManager.FEATURE_CHECK_ERROR)
+ return out
+ }
+ val dynamicModulesEnabled = context?.let { DynamicModuleSettings.isAvailable(it) } == true
+ if (dynamicModulesEnabled) runCatching { ChimeraConfigManager.reload() }
+ val messages = names.mapNotNull { name ->
+ if (!dynamicModulesEnabled) {
+ return@mapNotNull null
+ }
+ ChimeraConfigManager.featureConfigByKey(name)?.let { desc ->
+ val featureName = desc.featureName ?: return@let null
+ FeatureMessage.Builder()
+ .featureName(featureName)
+ .featureVersion(desc.featureVersion)
+ .build()
+ }
+ }
+ out.putByteArray("featuresResponseListKey", FeaturesMessage.Builder().features(messages).build().encode())
+ out.putInt("featuresResult", ModuleManager.FEATURE_CHECK_SUCCESS)
+ return out
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
@@ -82,8 +230,74 @@ class ServiceProvider : ContentProvider() {
return "vnd.android.cursor.item/com.google.android.gms.chimera"
}
+ private fun findModuleApkFile(moduleId: String): File? {
+ val chimeraModule = ChimeraConfigManager.findModuleByModuleId(moduleId)
+ resolveInstalledApkFile(chimeraModule)?.let { file ->
+ Log.d(TAG, "findModuleApkFile($moduleId): found via config: ${file.absolutePath}")
+ return file
+ }
+ return null
+ }
+
+ private fun resolveInstalledApkFile(chimeraModule: ChimeraModule?): File? {
+ val ctx = context ?: return null
+ val installedApkPath = chimeraModule?.installedApkPath?.takeIf { it.isNotEmpty() } ?: return null
+ // A signed APK may own several moduleIds whose canonical names differ. Its storage filename is
+ // only an artifact label, so a config-owned path must not be rejected by a filename/name mismatch.
+ return ChimeraStorage.verifiedModuleApk(
+ context = ctx,
+ file = File(installedApkPath),
+ expectedModuleName = null,
+ expectedSha256 = chimeraModule.apkSha256,
+ )
+ }
+
+ private fun rejectOpenFile(uri: Uri, reason: String): Nothing {
+ Log.w(TAG, "openFile rejected: $reason for $uri")
+ throw FileNotFoundException("No module APK for $uri ($reason)")
+ }
+
+ private fun isExpectedProviderUri(uri: Uri): Boolean = uri.authority == EXPECTED_AUTHORITY
+
+ private fun isValidModuleId(value: String): Boolean = isSafeModuleToken(value)
+
+ private fun isSafeModuleToken(value: String): Boolean {
+ return value.isNotEmpty() && value.length <= MAX_MODULE_TOKEN_LENGTH && MODULE_TOKEN_RE.matches(value)
+ }
+
companion object {
- private const val TAG = "ChimeraServiceProvider"
- private val COLUMNS = arrayOf("version", "apkPath", "loaderPath", "apkDescStr")
+ private const val TAG = "ServiceProvider"
+ private const val EXPECTED_AUTHORITY = "com.google.android.gms.chimera"
+ private const val MAX_MODULE_TOKEN_LENGTH = 200
+ private val MODULE_TOKEN_RE = Regex("[A-Za-z0-9_.-]+")
+ private val COLUMNS = arrayOf(
+ "version", "apkDesc", "loaderPath", "apkDescStr", "moduleConfig",
+ "moduleDescriptorIndex", "configLastModTime", "loaderVersion",
+ "requestStats", "dynamiteFlags", "disableStandaloneDynamiteLoader2", "apkSha256"
+ )
+
+ private fun buildModuleQueryCursor(
+ version: Int,
+ apkPath: String?,
+ configLastModified: Long,
+ apkSha256: String,
+ ): Cursor {
+ val cursor = MatrixCursor(COLUMNS, 1)
+ val row = arrayOfNulls(COLUMNS.size)
+ row[0] = version.toLong() // version
+ row[1] = null // apkDesc (blob)
+ row[2] = apkPath ?: "" // loaderPath
+ row[3] = apkPath ?: "" // apkDescStr
+ row[4] = null // moduleConfig
+ row[5] = 0L // moduleDescriptorIndex
+ row[6] = configLastModified // configLastModTime
+ row[7] = 3L // loaderVersion
+ row[8] = null // requestStats
+ row[9] = null // dynamiteFlags
+ row[10] = 0L // disableStandaloneDynamiteLoader2
+ row[11] = apkSha256 // content identity for client-side cache invalidation
+ cursor.addRow(row)
+ return cursor
+ }
}
}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt
new file mode 100644
index 0000000000..f6e7522801
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt
@@ -0,0 +1,328 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.moduleinstall
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.ChimeraModuleBootstrap
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.config.FeatureCheckUtils
+import com.google.android.chimera.config.FeatureMessage
+import com.google.android.chimera.config.FeaturesMessage
+import com.google.android.chimera.config.ModuleDownloadRegistry
+import com.google.android.chimera.config.ModuleManager
+import com.google.android.gms.common.Feature
+import com.google.android.gms.common.api.CommonStatusCodes
+import com.google.android.gms.common.api.Status
+import com.google.android.gms.common.api.internal.IStatusCallback
+import com.google.android.gms.common.internal.ConnectionInfo
+import com.google.android.gms.common.internal.GetServiceRequest
+import com.google.android.gms.common.internal.IGmsCallbacks
+import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse
+import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse.AvailabilityStatus.STATUS_ALREADY_AVAILABLE
+import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse.AvailabilityStatus.STATUS_READY_TO_DOWNLOAD
+import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse.AvailabilityStatus.STATUS_UNKNOWN_MODULE
+import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse
+import com.google.android.gms.common.moduleinstall.ModuleInstallResponse
+import com.google.android.gms.common.moduleinstall.ModuleInstallStatusCodes
+import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate
+import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest
+import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks
+import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService
+import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener
+import org.microg.gms.BaseService
+import org.microg.gms.common.GmsService
+import org.microg.gms.common.PackageUtils
+import org.microg.gms.ui.MainSettingsActivity
+import java.util.concurrent.atomic.AtomicInteger
+
+private const val TAG = "GmsModule/Service"
+private const val MODULE_ACTION_REQUIRED_MESSAGE =
+ "Interactive module download or import is required."
+private const val ACTION_REQUEST_FEATURES_WITH_UI = "com.google.android.chimera.container.REQUEST_FEATURES_WITH_UI"
+private const val EXTRA_CHIMERA_FEATURE_LIST = "chimera.FEATURE_LIST"
+private const val EXTRA_CHIMERA_REQUESTER_PACKAGE = "chimera.REQUESTER_PACKAGE"
+private const val EXTRA_OFFICIAL_REQUESTER_PACKAGE = "get_module_install_request_package"
+private const val EXTRA_REQUESTED_FEATURE_NAMES = "org.microg.gms.moduleinstall.REQUESTED_FEATURE_NAMES"
+private const val EXTRA_REQUESTED_FEATURE_VERSIONS = "org.microg.gms.moduleinstall.REQUESTED_FEATURE_VERSIONS"
+private const val EXTRA_REQUESTER_PACKAGE = "org.microg.gms.moduleinstall.REQUESTER_PACKAGE"
+
+class ModuleInstallService : BaseService(TAG, GmsService.MODULE_INSTALL) {
+ override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
+ val callingPackage = PackageUtils.getAndCheckCallingPackage(this, request.packageName)
+ ?: throw IllegalArgumentException("Missing package name")
+ val binder = ModuleInstallServiceImpl(this, callingPackage, lifecycle).asBinder()
+ callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, binder, ConnectionInfo().apply {
+ features = arrayOf(Feature("moduleinstall", 7))
+ })
+ }
+}
+
+/**
+ * Reports installed feature availability and supplies the catalog-backed permission/download UI when a
+ * client requests an install intent. Background install calls never bypass that user interaction.
+ */
+class ModuleInstallServiceImpl(
+ private val context: Context,
+ private val callingPackage: String,
+ override val lifecycle: Lifecycle
+) : IModuleInstallService.Stub(), LifecycleOwner {
+
+ private fun checkAvailability(request: ApiFeatureRequest?): Int {
+ // Module imports are performed by the UI process, while ModuleInstall normally runs in the
+ // main GMS process. Initialize AppContext in this process before refreshing the persisted
+ // manifest; reload() intentionally returns an empty manifest while AppContext is uninitialized.
+ ChimeraModuleBootstrap.ensureInitialized(context)
+ runCatching { ChimeraConfigManager.reload() }
+ .onFailure { Log.w(TAG, "checkAvailability: failed to reload Chimera config", it) }
+
+ val featureCheck = ModuleManager.FeatureCheck()
+ for (feature in request?.features ?: emptyList()) {
+ val name = feature.name?.takeIf { it.isNotEmpty() } ?: return ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE
+ if (feature.version < -1L) {
+ Log.w(TAG, "checkAvailability: invalid requested version for $name: ${feature.version}")
+ return ModuleManager.FEATURE_CHECK_ERROR
+ }
+ featureCheck.checkFeatureAtVersion(name, feature.version)
+ }
+ // Preserve the legacy optimistic response for features that are not in our download
+ // catalog. Built-in and fallback Dynamite implementations are not exhaustively described
+ // by feature aliases, so rejecting an unknown alias can disable otherwise working APIs.
+ // Catalog-backed .mods features still need their real state so clients can request install UI.
+ for (descriptor in featureCheck.featureDescriptors) {
+ if (!ModuleDownloadRegistry.isKnownDynamicFeature(descriptor.featureName)) {
+ Log.d(TAG, "checkAvailability: treating unregistered feature as already available: ${descriptor.featureName}")
+ continue
+ }
+ val result = FeatureCheckUtils.checkFeatureDescriptors(
+ listOf(descriptor),
+ allowStaticRegistry = false,
+ allowDynamicModules = DynamicModuleSettings.isAvailable(context)
+ )
+ if (result != ModuleManager.FEATURE_CHECK_SUCCESS) return result
+ }
+ return ModuleManager.FEATURE_CHECK_SUCCESS
+ }
+
+ private fun getEffectivePackage(request: ApiFeatureRequest?): String {
+ val requestedPackage = request?.callingPackage?.takeUnless { it.isEmpty() }
+ if (requestedPackage != null && requestedPackage != callingPackage) {
+ Log.w(TAG, "Ignoring spoofed ModuleInstall callingPackage=$requestedPackage from bound package=$callingPackage")
+ }
+ return callingPackage
+ }
+
+ private fun requestedFeatures(request: ApiFeatureRequest?): List = request?.features ?: emptyList()
+
+ override fun areModulesAvailable(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?) {
+ Log.d(TAG, "areModulesAvailable: $request")
+ val result = checkAvailability(request)
+ val response = when (result) {
+ ModuleManager.FEATURE_CHECK_SUCCESS ->
+ ModuleAvailabilityResponse(true, STATUS_ALREADY_AVAILABLE)
+
+ ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE ->
+ ModuleAvailabilityResponse(false, STATUS_UNKNOWN_MODULE)
+
+ ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED ->
+ ModuleAvailabilityResponse(false, STATUS_READY_TO_DOWNLOAD)
+
+ else -> null
+ }
+ if (response == null) {
+ Log.w(TAG, "areModulesAvailable: internal error, result=$result")
+ runCatching {
+ callbacks?.onModuleAvailabilityResponse(
+ Status(CommonStatusCodes.INTERNAL_ERROR, "Internal error while attempting to perform the availability check"),
+ null
+ )
+ }
+ return
+ }
+ runCatching { callbacks?.onModuleAvailabilityResponse(Status.SUCCESS, response) }
+ }
+
+ override fun installModules(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?, listener: IModuleInstallStatusListener?) {
+ Log.d(TAG, "installModules: request=$request, urgent=${request?.urgent}")
+ if (request?.urgent != true) {
+ deferredInstall(callbacks, request)
+ return
+ }
+ // A background request cannot grant permissions or launch the chooser; missing modules require the
+ // separate install-intent flow below.
+ when (val result = checkAvailability(request)) {
+ ModuleManager.FEATURE_CHECK_SUCCESS -> {
+ runCatching {
+ callbacks?.onModuleInstallResponse(Status.SUCCESS, ModuleInstallResponse(0, false))
+ }
+ }
+
+ ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE,
+ ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED -> {
+ val sessionId = registerListener(listener)
+ if (sessionId != 0) {
+ // Acknowledge with a real session, then deliver a terminal failure so clients waiting on
+ // background progress do not hang while user interaction is still required.
+ runCatching {
+ callbacks?.onModuleInstallResponse(Status.SUCCESS, ModuleInstallResponse(sessionId, true))
+ }
+ notifyInstallFailed(sessionId, listener, result)
+ } else {
+ runCatching {
+ callbacks?.onModuleInstallResponse(
+ Status(CommonStatusCodes.API_NOT_CONNECTED, MODULE_ACTION_REQUIRED_MESSAGE),
+ null
+ )
+ }
+ }
+ }
+
+ else -> {
+ runCatching {
+ callbacks?.onModuleInstallResponse(
+ Status(CommonStatusCodes.INTERNAL_ERROR, "Internal error while attempting to start module install"),
+ null
+ )
+ }
+ }
+ }
+ }
+
+ private fun deferredInstall(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?) {
+ if (requestedFeatures(request).isEmpty()) {
+ Log.w(TAG, "deferredInstall: no valid features in request=$request")
+ runCatching { callbacks?.onStatus(Status(CommonStatusCodes.INTERNAL_ERROR)) }
+ return
+ }
+ val status = when (val result = checkAvailability(request)) {
+ ModuleManager.FEATURE_CHECK_SUCCESS -> Status.SUCCESS
+ ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE ->
+ Status(ModuleInstallStatusCodes.UNKNOWN_MODULE, MODULE_ACTION_REQUIRED_MESSAGE)
+
+ ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED ->
+ Status(ModuleInstallStatusCodes.MODULE_NOT_FOUND, MODULE_ACTION_REQUIRED_MESSAGE)
+
+ else -> {
+ Log.w(TAG, "deferredInstall: internal error, result=$result")
+ Status(CommonStatusCodes.INTERNAL_ERROR)
+ }
+ }
+ runCatching { callbacks?.onStatus(status) }
+ }
+
+ override fun getInstallModulesIntent(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?) {
+ Log.d(TAG, "getInstallModulesIntent: $request")
+ val result = checkAvailability(request)
+ val response = when (result) {
+ ModuleManager.FEATURE_CHECK_SUCCESS -> ModuleInstallIntentResponse(null)
+ ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE,
+ ModuleManager.FEATURE_CHECK_UPDATE_REQUIRED -> ModuleInstallIntentResponse(createInstallModulesPendingIntent(request))
+
+ else -> null
+ }
+ val status = if (response != null) {
+ Status.SUCCESS
+ } else {
+ Status(CommonStatusCodes.INTERNAL_ERROR, "Internal error while attempting to build the install intent")
+ }
+ runCatching { callbacks?.onModuleInstallIntentResponse(status, response) }
+ }
+
+ private fun createInstallModulesPendingIntent(request: ApiFeatureRequest?): PendingIntent {
+ val features = requestedFeatures(request).filter { !it.name.isNullOrEmpty() }
+ val featureNames = ArrayList(features.map { checkNotNull(it.name) })
+ val featureVersions = features.map { it.version }.toLongArray()
+ val requesterPackage = getEffectivePackage(request)
+ val requestedDynamicFeatures = features.map {
+ ModuleDownloadRegistry.RequestedFeature(checkNotNull(it.name), it.version)
+ }
+ val availabilityRequest = ApiFeatureRequest().apply {
+ this.features = features
+ }
+ val intent = ModuleDownloadRegistry.createModuleDownloadIntentForRequests(
+ context,
+ requestedDynamicFeatures,
+ availabilityRequest,
+ ) ?: run {
+ Intent(context, MainSettingsActivity::class.java).apply {
+ action = ACTION_REQUEST_FEATURES_WITH_UI
+ putExtra(MainSettingsActivity.EXTRA_OPEN_DYNAMIC_MODULE_MANAGER, true)
+ putExtra(EXTRA_CHIMERA_FEATURE_LIST, encodeFeatureList(features))
+ putExtra(EXTRA_CHIMERA_REQUESTER_PACKAGE, requesterPackage)
+ putExtra(EXTRA_OFFICIAL_REQUESTER_PACKAGE, requesterPackage)
+ putStringArrayListExtra(EXTRA_REQUESTED_FEATURE_NAMES, featureNames)
+ putExtra(EXTRA_REQUESTED_FEATURE_VERSIONS, featureVersions)
+ putExtra(EXTRA_REQUESTER_PACKAGE, requesterPackage)
+ }
+ }
+ intent.apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
+ val requestCode = 31 * requesterPackage.hashCode() + requestedDynamicFeatures.hashCode()
+ return PendingIntent.getActivity(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ private fun encodeFeatureList(features: List): ByteArray {
+ return FeaturesMessage.Builder()
+ .features(features.mapNotNull { feature ->
+ val name = feature.name?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null
+ FeatureMessage.Builder()
+ .featureName(name)
+ .featureVersion(feature.version)
+ .build()
+ })
+ .build()
+ .encode()
+ }
+
+ override fun releaseModules(callback: IStatusCallback?, request: ApiFeatureRequest?) {
+ Log.d(TAG, "releaseModules: $request")
+ val features = request?.features
+ if (features.isNullOrEmpty() || features.any { it.name.isNullOrEmpty() }) {
+ runCatching { callback?.onResult(Status(CommonStatusCodes.INTERNAL_ERROR)) }
+ return
+ }
+ getEffectivePackage(request)
+ runCatching { callback?.onResult(Status.SUCCESS) }
+ }
+
+ override fun unregisterListener(callback: IStatusCallback?, listener: IModuleInstallStatusListener?) {
+ Log.d(TAG, "unregisterListener")
+ runCatching { callback?.onResult(Status.SUCCESS) }
+ }
+
+ private fun registerListener(listener: IModuleInstallStatusListener?): Int =
+ if (listener?.asBinder()?.isBinderAlive == true) nextSessionId.getAndIncrement() else 0
+
+ private fun notifyInstallFailed(sessionId: Int, listener: IModuleInstallStatusListener?, featureCheckResult: Int) {
+ val errorCode = when (featureCheckResult) {
+ ModuleManager.FEATURE_CHECK_UNKNOWN_FEATURE -> ModuleInstallStatusCodes.UNKNOWN_MODULE
+ else -> ModuleInstallStatusCodes.MODULE_NOT_FOUND
+ }
+ runCatching {
+ listener?.onModuleInstallStatusUpdate(
+ ModuleInstallStatusUpdate(sessionId, INSTALL_STATE_FAILED, null, null, errorCode)
+ )
+ }.onFailure {
+ Log.w(TAG, "notifyInstallFailed: failed to notify listener for sessionId=$sessionId", it)
+ }
+ }
+
+ companion object {
+ private const val INSTALL_STATE_FAILED = 5
+ private val nextSessionId = AtomicInteger(1)
+ }
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstaller.kt b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstaller.kt
new file mode 100644
index 0000000000..bc87d06301
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstaller.kt
@@ -0,0 +1,151 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.microg.gms.moduleinstall
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.ChimeraModuleBootstrap
+import com.google.android.chimera.config.ChimeraStorage
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.loader.ChimeraModuleLdr
+import com.google.android.gms.chimera.DynamiteContextFactory
+import com.google.android.gms.chimera.ModuleInfo
+import com.google.android.gms.common.app.AppContext
+import java.io.File
+import java.io.IOException
+import java.security.MessageDigest
+import java.util.UUID
+
+/** Atomic persistence boundary for already-validated dynamic-module APKs. */
+internal object ModuleInstaller {
+ private const val TAG = "GmsModule/Installer"
+
+ data class Request(
+ val sourceApk: File,
+ val moduleName: String,
+ val version: Long,
+ val oldApkPaths: Set,
+ val invalidateModuleIds: Set,
+ )
+
+ /**
+ * Stages every request, commits all registrations together, and rolls back unreferenced files if
+ * persistence fails. Cleanup and cache invalidation happen only after the durable commit.
+ */
+ @Synchronized
+ internal fun installBatch(context: Context, requests: List) {
+ check(DynamicModuleSettings.isAvailable(context)) {
+ "Dynamic modules are unavailable on this device or disabled by the user"
+ }
+ if (requests.isEmpty()) return
+ val staged = ArrayList(requests.size)
+ val createdFiles = ArrayList(requests.size)
+ try {
+ requests.forEach { request -> staged += stage(context, request, createdFiles) }
+ registerInstalled(context, staged)
+ } catch (error: Exception) {
+ runCatching { ChimeraConfigManager.reload() }
+ createdFiles.forEach { file ->
+ if (!ChimeraConfigManager.isApkPathReferenced(file.absolutePath)) {
+ ChimeraStorage.safeDeleteModuleApk(context, file)
+ }
+ }
+ throw error
+ }
+
+ requests.flatMap(Request::oldApkPaths).distinct().forEach { oldPath ->
+ runCatching {
+ if (!ChimeraConfigManager.isApkPathReferenced(oldPath)) {
+ ChimeraStorage.safeDeleteModuleApk(context, File(oldPath))
+ }
+ }.onFailure { Log.w(TAG, "Unable to remove obsolete module artifact") }
+ }
+ requests.flatMap { request ->
+ request.invalidateModuleIds.map { moduleId -> moduleId to request.moduleName }
+ }.distinct()
+ .forEach { (moduleId, moduleName) ->
+ runCatching { invalidateRuntimeCaches(moduleId, moduleName, null) }
+ .onFailure { Log.w(TAG, "Unable to invalidate runtime cache for $moduleId") }
+ }
+ }
+
+ private fun stage(
+ context: Context,
+ request: Request,
+ createdFiles: MutableList,
+ ): ModuleInfo {
+ val destination = ChimeraStorage.allocateModuleApkFile(
+ context,
+ request.moduleName,
+ request.version.toString(),
+ )
+ val finalFile = destination.file
+ val tempFile = File(finalFile.parentFile, "${finalFile.name}.tmp-${UUID.randomUUID()}")
+ try {
+ request.sourceApk.copyTo(tempFile, overwrite = false)
+ if (!tempFile.renameTo(finalFile)) {
+ throw IOException("Failed to finalize module APK")
+ }
+ // Track the finalized file before permission or digest work so any later failure can roll it back.
+ createdFiles += finalFile
+ ChimeraStorage.makeModuleApkReadable(finalFile)
+ return ModuleInfo(
+ source = Uri.fromFile(finalFile).toString(),
+ module_name = request.moduleName,
+ module_version = request.version.toString(),
+ filename = finalFile.name,
+ sha256_hash = sha256Hex(finalFile),
+ priority = destination.priority,
+ )
+ } finally {
+ tempFile.delete()
+ }
+ }
+
+ private fun registerInstalled(context: Context, modules: List) {
+ ChimeraModuleBootstrap.ensureInitialized(context)
+ if (!AppContext.isInitialized()) {
+ (context.applicationContext as? android.app.Application)?.let(AppContext::init)
+ }
+ ChimeraConfigManager.updateModuleDownload(modules)
+
+ val persisted = ChimeraConfigManager.reload()
+ val missing = modules.filterNot { info ->
+ val expectedPath = info.source?.let { Uri.parse(it).path }.orEmpty()
+ val expectedDigest = info.sha256_hash.orEmpty()
+ expectedPath.isNotEmpty() && persisted.chimeraModules.any { module ->
+ module.installedApkPath == expectedPath &&
+ (expectedDigest.isEmpty() || module.apkSha256 == expectedDigest)
+ }
+ }
+ check(missing.isEmpty()) {
+ "Module registration was not persisted: ${missing.map { it.module_name }}"
+ }
+ }
+
+ private fun sha256Hex(file: File): String {
+ val digest = MessageDigest.getInstance("SHA-256")
+ file.inputStream().buffered().use { input ->
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ while (true) {
+ val count = input.read(buffer)
+ if (count < 0) break
+ digest.update(buffer, 0, count)
+ }
+ }
+ return digest.digest().joinToString("") { "%02x".format(it) }
+ }
+
+ fun invalidateRuntimeCaches(
+ moduleId: String? = null,
+ moduleName: String? = null,
+ apkPath: String? = null,
+ ) {
+ ChimeraModuleLdr.clearModuleCache(moduleId, moduleName, apkPath)
+ if (!moduleId.isNullOrEmpty()) DynamiteContextFactory.clearCacheForModule(moduleId)
+ }
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ApkSignatureVerifier.kt b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ApkSignatureVerifier.kt
new file mode 100644
index 0000000000..bdfced2376
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ApkSignatureVerifier.kt
@@ -0,0 +1,95 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.microg.gms.moduleinstall.dynamicmodule
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.Signature
+import android.os.Build
+import android.util.Log
+import org.microg.gms.common.KNOWN_GOOGLE_CERT_SHA256
+import java.io.File
+import java.security.MessageDigest
+
+/**
+ * Import-time trust gate: a module apk is accepted only if it is signed *solely* by Google.
+ *
+ * Trust is per-apk, not per-container: every module apk inside the imported bundle must itself carry a
+ * Google signing certificate (the bundle as a whole is not signed). Google signs its GMS/Chimera modules
+ * with more than one key (the main GMS release cert for MLKit-style modules, the `gmscore_modules_percy`
+ * cert for Dynamite/Chimera integ modules, etc.), so the allowlist holds every known Google module cert.
+ *
+ * On API 28+, getPackageArchiveInfo runs PackageParser.collectCertificates, which cryptographically
+ * verifies the apk's v2/v3 (or v1) signature against the apk contents before returning the certificate —
+ * so a non-Google apk cannot make the PackageManager report a Google cert it does not actually carry.
+ */
+object ApkSignatureVerifier {
+ private const val TAG = "GmsModule/ApkSig"
+
+ /**
+ * SHA-256 (lowercase hex) of every Google signing certificate accepted for module import. Reuses the
+ * shared Google cert allowlist [KNOWN_GOOGLE_CERT_SHA256] (the privileged platform certs `f0fd6c5b…`,
+ * `7ce83c1b…` + the official-apps/DroidGuard cert `3d7a1223…`) — one source of truth with the rest of
+ * GmsCore — plus the module-only `gmscore_modules_percy` cert `afee62fb…`, which signs Dynamite/Chimera
+ * integ modules but no Google app (so it is intentionally not in KnownGooglePackages). All confirmed with
+ * keytool against the genuine GMS module apks. An unexpected reject of a known-good Google bundle prints
+ * the offending signer hash via [isGoogleCert]'s Log.d — the signal to add it.
+ */
+ private val GOOGLE_CERT_SHA256: Set = KNOWN_GOOGLE_CERT_SHA256 +
+ "afee62fb653c9d863d1a6d6046b35225bf6380dd4657b105e8331a1de49f9f91"
+
+ /**
+ * Returns true iff [apkFile] is signed *solely* by Google. The apk's signing certificate(s) are read
+ * without installing it; for a multi-signer apk EVERY signer must be a known Google cert (a single
+ * Google signer alongside an attacker key is rejected), while for a single-signer apk any cert in its
+ * v3 signing lineage being Google is sufficient (so Google key-rotation does not false-reject).
+ */
+ fun isGoogleSignedApk(context: Context, apkFile: File): Boolean {
+ val pm = context.packageManager
+ val path = apkFile.absolutePath
+ return try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ val info = pm.getPackageArchiveInfo(path, PackageManager.GET_SIGNING_CERTIFICATES)
+ val si = info?.signingInfo ?: return logNoCert(apkFile)
+ if (si.hasMultipleSigners()) {
+ // Multi-signer: a trust gate must require EVERY current signer to be Google, otherwise an
+ // apk co-signed by Google plus an attacker key would pass.
+ val signers = si.apkContentsSigners
+ signers.isNotEmpty() && signers.all { isGoogleCert(it, apkFile) }
+ } else {
+ // Single-signer: use signingCertificateHistory (intentional — NOT apkContentsSigners) so an
+ // allowlisted ancestor still matches after Google rotates the key. The history is the v3
+ // signing lineage, cryptographically chained, so an attacker cannot insert a Google cert
+ // into it without Google's private key; matching any lineage cert is therefore safe here.
+ si.signingCertificateHistory?.any { isGoogleCert(it, apkFile) } ?: logNoCert(apkFile)
+ }
+ } else {
+ // Pre-P: only the v1 (JAR) signing certificate is available. Require every signer to be Google.
+ @Suppress("DEPRECATION")
+ val info = pm.getPackageArchiveInfo(path, PackageManager.GET_SIGNATURES)
+ @Suppress("DEPRECATION")
+ val sigs = info?.signatures ?: return logNoCert(apkFile)
+ sigs.isNotEmpty() && sigs.all { isGoogleCert(it, apkFile) }
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "failed to read signature of ${apkFile.name}: ${e.message}")
+ false
+ }
+ }
+
+ /** True iff [sig]'s certificate SHA-256 is in the Google allowlist; logs the hash otherwise. */
+ private fun isGoogleCert(sig: Signature, apkFile: File): Boolean {
+ val hex = MessageDigest.getInstance("SHA-256").digest(sig.toByteArray())
+ .joinToString("") { "%02x".format(it) }
+ if (hex in GOOGLE_CERT_SHA256) return true
+ Log.d(TAG, "non-Google signer in ${apkFile.name}: $hex")
+ return false
+ }
+
+ private fun logNoCert(apkFile: File): Boolean {
+ Log.w(TAG, "no signing certificate found in ${apkFile.name}")
+ return false
+ }
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsContainer.kt b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsContainer.kt
new file mode 100644
index 0000000000..7277db6dbb
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsContainer.kt
@@ -0,0 +1,117 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.microg.gms.moduleinstall.dynamicmodule
+
+import android.util.Log
+import com.google.android.chimera.config.ChimeraApkIdentity
+import com.google.android.chimera.config.ChimeraApkManifestReader
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.zip.ZipFile
+
+/** One module APK extracted from a `.mods` container with its signed Chimera identities. */
+internal data class ModsApk(
+ val apkFile: File,
+ val identities: List,
+)
+
+/** Parses and extracts the immutable `.mods` container format without trusting mapping identities. */
+internal object ModsContainer {
+ private const val TAG = "GmsModule/Mods"
+ private const val ENTRY_MAPPING = "mapping.json"
+ private const val MAX_APK_BYTES = 256L * 1024 * 1024
+ private const val MAX_TOTAL_APK_BYTES = 512L * 1024 * 1024
+ private const val MAX_APK_COUNT = 64
+ private const val MAX_MAPPING_BYTES = 1024L * 1024
+
+ /**
+ * Collects every APK referenced by the feature mapping, extracts it under a fixed filename, and
+ * reads its authoritative module identities from the signed APK manifest.
+ */
+ fun open(mods: File, destDir: File): List {
+ require(destDir.exists() || destDir.mkdirs()) { "mods: unable to create extraction directory" }
+ ZipFile(mods).use { zip ->
+ val mappingEntry = zip.getEntry(ENTRY_MAPPING)
+ ?: throw IllegalArgumentException("mods: missing '$ENTRY_MAPPING'")
+ val mapping = JSONObject(
+ zip.getInputStream(mappingEntry).use { readBoundedText(it, MAX_MAPPING_BYTES) }
+ )
+ require(mapping.optString("format").startsWith("mods/")) {
+ "mods: unsupported or missing format"
+ }
+ val features = mapping.optJSONArray("features")
+ ?: throw IllegalArgumentException("mods: mapping.json missing 'features'")
+
+ val apkPaths = LinkedHashSet()
+ for (featureIndex in 0 until features.length()) {
+ val feature = features.optJSONObject(featureIndex)
+ if (feature == null) {
+ Log.w(TAG, "mods: features[$featureIndex] is not an object, skipping")
+ continue
+ }
+ val paths = feature.optJSONArray("apks")
+ if (paths == null) {
+ Log.w(TAG, "mods: features[$featureIndex] has no 'apks' array, skipping")
+ continue
+ }
+ for (pathIndex in 0 until paths.length()) {
+ val path = paths.optString(pathIndex)
+ require(
+ path.isNotEmpty() &&
+ !path.contains("..") &&
+ !path.startsWith("/") &&
+ !path.contains('\\')
+ ) { "mods: unsafe apk path '$path'" }
+ require(path.startsWith("apks/") && path.endsWith(".apk", ignoreCase = true)) {
+ "mods: invalid apk entry '$path'"
+ }
+ apkPaths += path
+ require(apkPaths.size <= MAX_APK_COUNT) {
+ "mods: too many apks (max $MAX_APK_COUNT)"
+ }
+ }
+ }
+ require(apkPaths.isNotEmpty()) { "mods: no apks referenced" }
+
+ var totalExtracted = 0L
+ return apkPaths.mapIndexed { index, path ->
+ val entry = zip.getEntry(path)
+ ?: throw IllegalArgumentException("mods: missing apk '$path'")
+ val output = File(destDir, "module_$index.apk")
+ zip.getInputStream(entry).use { input ->
+ val entryLimit = minOf(MAX_APK_BYTES, MAX_TOTAL_APK_BYTES - totalExtracted)
+ totalExtracted += output.outputStream().use { copyBounded(input, it, entryLimit) }
+ }
+ ModsApk(
+ apkFile = output,
+ identities = ChimeraApkManifestReader.readIdentities(output),
+ )
+ }
+ }
+ }
+}
+
+private fun readBoundedText(input: InputStream, limit: Long): String =
+ ByteArrayOutputStream().use { output ->
+ copyBounded(input, output, limit)
+ output.toString(Charsets.UTF_8.name())
+ }
+
+/** Copies a stream while aborting as soon as the byte cap is exceeded. */
+internal fun copyBounded(input: InputStream, output: OutputStream, limit: Long): Long {
+ require(limit >= 0L) { "negative byte limit" }
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ var total = 0L
+ while (true) {
+ val count = input.read(buffer)
+ if (count < 0) return total
+ total += count
+ require(total <= limit) { "entry exceeds $limit bytes" }
+ output.write(buffer, 0, count)
+ }
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsImporter.kt b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsImporter.kt
new file mode 100644
index 0000000000..f6ba494ecd
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsImporter.kt
@@ -0,0 +1,335 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.microg.gms.moduleinstall.dynamicmodule
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import com.google.android.chimera.config.ChimeraApkManifestReader
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.config.registry.DynamicModuleRegistry
+import org.microg.gms.moduleinstall.ModuleInstaller
+import java.io.File
+import java.io.FileOutputStream
+import java.util.UUID
+
+private const val TAG = "GmsModule/Mods"
+private const val MAX_CONTAINER_BYTES = 512L * 1024 * 1024
+
+internal enum class VersionDecision {
+ INSTALL,
+ SKIP_SAME,
+ OVERWRITE,
+ SKIP_LOWER,
+ SKIP_DUPLICATE,
+}
+
+internal enum class ModsImportFailure {
+ UNAVAILABLE,
+ SOURCE_UNAVAILABLE,
+ INVALID_CONTAINER,
+ VALIDATION_FAILED,
+ TRANSACTION_FAILED,
+}
+
+internal data class ModsImportResult(
+ val decisions: List = emptyList(),
+ val rejected: Int = 0,
+ val failure: ModsImportFailure? = null,
+) {
+ val installed
+ get() = decisions.count {
+ it == VersionDecision.INSTALL || it == VersionDecision.OVERWRITE
+ }
+ val skipped
+ get() = decisions.count {
+ it == VersionDecision.SKIP_SAME ||
+ it == VersionDecision.SKIP_LOWER ||
+ it == VersionDecision.SKIP_DUPLICATE
+ }
+ val accepted
+ get() = failure == null && decisions.isNotEmpty() && rejected == 0 &&
+ decisions.any { it != VersionDecision.SKIP_DUPLICATE }
+}
+
+/** Complete `.mods` import use case: bounded URI ingestion, validation, planning, and atomic commit. */
+internal object ModsImporter {
+ private data class PlannedArtifact(
+ val apk: ModsApk,
+ val moduleName: String,
+ val version: Long,
+ val decision: VersionDecision,
+ val oldApkPaths: Set,
+ )
+
+ @Synchronized
+ fun importFrom(context: Context, source: Uri): ModsImportResult {
+ if (!DynamicModuleSettings.isAvailable(context)) {
+ Log.d(TAG, "Dynamic modules unavailable; refusing import")
+ return ModsImportResult(failure = ModsImportFailure.UNAVAILABLE)
+ }
+ return importTemporaryContainer(context, copySourceToTemp(context, source))
+ }
+
+ private fun importTemporaryContainer(context: Context, container: File?): ModsImportResult {
+ container ?: return ModsImportResult(failure = ModsImportFailure.SOURCE_UNAVAILABLE)
+ return try {
+ importContainer(context, container)
+ } finally {
+ container.delete()
+ }
+ }
+
+ private fun importContainer(context: Context, container: File): ModsImportResult {
+ val extractionDir = File(context.cacheDir, "mods_import/${UUID.randomUUID()}")
+ val apks = try {
+ ModsContainer.open(container, extractionDir)
+ } catch (error: Exception) {
+ Log.w(TAG, "Invalid .mods container")
+ runCatching { extractionDir.deleteRecursively() }
+ return ModsImportResult(failure = ModsImportFailure.INVALID_CONTAINER)
+ }
+
+ try {
+ if (!validateAllArtifacts(context, apks)) {
+ return rejected(apks, ModsImportFailure.VALIDATION_FAILED)
+ }
+ val selected = selectNonConflictingArtifacts(apks)
+ ?: return rejected(apks, ModsImportFailure.VALIDATION_FAILED)
+
+ ChimeraConfigManager.reload()
+ if (!validateComponentRoutes(apks, selected)) {
+ return rejected(apks, ModsImportFailure.VALIDATION_FAILED)
+ }
+ val plans = apks.map { apk ->
+ if (apk !in selected) {
+ PlannedArtifact(
+ apk = apk,
+ moduleName = canonicalArtifactName(apk),
+ version = artifactVersion(apk),
+ decision = VersionDecision.SKIP_DUPLICATE,
+ oldApkPaths = emptySet(),
+ )
+ } else {
+ createPlan(apk)
+ }
+ }
+ val installRequests = plans.mapNotNull { plan ->
+ if (plan.decision != VersionDecision.INSTALL &&
+ plan.decision != VersionDecision.OVERWRITE
+ ) return@mapNotNull null
+ ModuleInstaller.Request(
+ sourceApk = plan.apk.apkFile,
+ moduleName = plan.moduleName,
+ version = plan.version,
+ oldApkPaths = plan.oldApkPaths,
+ invalidateModuleIds = if (plan.decision == VersionDecision.OVERWRITE) {
+ plan.apk.identities.mapNotNull { it.moduleId }.toSet()
+ } else {
+ emptySet()
+ },
+ )
+ }
+ ModuleInstaller.installBatch(context, installRequests)
+ plans.forEach { plan ->
+ Log.d(TAG, "Import ${plan.apk.identities.mapNotNull { it.moduleId }} decision=${plan.decision}")
+ }
+ return ModsImportResult(decisions = plans.map(PlannedArtifact::decision))
+ } catch (error: Exception) {
+ Log.w(TAG, "Atomic import failed")
+ return rejected(apks, ModsImportFailure.TRANSACTION_FAILED)
+ } finally {
+ runCatching { extractionDir.deleteRecursively() }
+ }
+ }
+
+ private fun copySourceToTemp(context: Context, source: Uri): File? {
+ val output = runCatching { File.createTempFile("import_", ".mods", context.cacheDir) }
+ .getOrElse {
+ Log.w(TAG, "Unable to allocate import file")
+ return null
+ }
+ return try {
+ val input = context.contentResolver.openInputStream(source)
+ ?: throw IllegalArgumentException("Unable to open module container")
+ input.use { sourceStream ->
+ FileOutputStream(output).use { destination ->
+ copyBounded(sourceStream, destination, MAX_CONTAINER_BYTES)
+ }
+ }
+ output.takeIf { it.length() > 0L }
+ } catch (error: Exception) {
+ Log.w(TAG, "Unable to ingest module container")
+ null
+ }.also { result ->
+ if (result == null) output.delete()
+ }
+ }
+
+ private fun validateAllArtifacts(context: Context, apks: List): Boolean {
+ if (apks.isEmpty()) return false
+ return apks.all { apk ->
+ val identities = apk.identities
+ val validIdentities = identities.isNotEmpty() &&
+ identities.mapNotNull { it.moduleId }.distinct().size == identities.size &&
+ identities.all { !it.moduleId.isNullOrEmpty() && (it.moduleVersion ?: 0) > 0 }
+ when {
+ !validIdentities -> {
+ Log.w(TAG, "Reject artifact: invalid Chimera identities")
+ false
+ }
+
+ !ApkSignatureVerifier.isGoogleSignedApk(context, apk.apkFile) -> {
+ Log.w(TAG, "Reject artifact: untrusted signer")
+ false
+ }
+
+ !hasValidComponentBindings(apk) -> false
+ else -> true
+ }
+ }
+ }
+
+ /** Validates route structure without restricting future Google-signed module IDs. */
+ private fun hasValidComponentBindings(apk: ModsApk): Boolean {
+ val manifests = ChimeraApkManifestReader.readModuleManifests(apk.apkFile) ?: run {
+ Log.w(TAG, "Reject artifact: unreadable Chimera manifests")
+ return false
+ }
+ val valid = manifests.all { manifest ->
+ (
+ manifest.activityBindings +
+ manifest.boundServiceBindings +
+ manifest.providerBindings +
+ manifest.sliceProviderBindings
+ ).all { binding ->
+ binding.containerName?.isValidRouteName() == true &&
+ binding.moduleChimeraName?.isValidRouteName() == true
+ }
+ }
+ if (!valid) Log.w(TAG, "Reject artifact: invalid component binding")
+ return valid
+ }
+
+ /** Rejects ambiguous routes rather than letting import order choose the module implementation. */
+ private fun validateComponentRoutes(apks: List, selected: Set): Boolean {
+ val activityRoutes = linkedMapOf()
+ val serviceRoutes = linkedMapOf()
+ for (apk in apks) {
+ if (apk !in selected) continue
+ for (manifest in ChimeraApkManifestReader.readModuleManifests(apk.apkFile).orEmpty()) {
+ val moduleId = manifest.moduleId ?: return false
+ for (binding in manifest.activityBindings) {
+ val containerName = binding.containerName ?: return false
+ if (!validateRoute(activityRoutes, containerName, moduleId) ||
+ ChimeraConfigManager.findComponentByComponentName(containerName)
+ ?.moduleId
+ ?.let { it != moduleId } == true
+ ) {
+ Log.w(TAG, "Reject conflicting activity route $containerName")
+ return false
+ }
+ }
+ for (binding in manifest.boundServiceBindings) {
+ val containerName = binding.containerName ?: return false
+ if (!validateRoute(serviceRoutes, containerName, moduleId) ||
+ ChimeraConfigManager.findChimeraBoundService(containerName)
+ ?.moduleId
+ ?.let { it != moduleId } == true
+ ) {
+ Log.w(TAG, "Reject conflicting service route $containerName")
+ return false
+ }
+ }
+ }
+ }
+ return true
+ }
+
+ private fun validateRoute(routes: MutableMap, containerName: String, moduleId: String): Boolean {
+ return routes.getOrPut(containerName) { moduleId } == moduleId
+ }
+
+ /** Selects one whole APK per duplicated module ID and rejects ambiguous multi-ID overlap. */
+ private fun selectNonConflictingArtifacts(apks: List): Set? {
+ val bestByModuleId = HashMap()
+ apks.forEach { apk ->
+ apk.identities.forEach { identity ->
+ val moduleId = checkNotNull(identity.moduleId)
+ val current = bestByModuleId[moduleId]
+ if (current == null || (identity.moduleVersion ?: 0) >
+ (current.identities.first { it.moduleId == moduleId }.moduleVersion ?: 0)
+ ) {
+ bestByModuleId[moduleId] = apk
+ }
+ }
+ }
+ val selected = LinkedHashSet()
+ apks.forEach { apk ->
+ val wins = apk.identities.count { bestByModuleId[it.moduleId] === apk }
+ if (wins == apk.identities.size) {
+ selected += apk
+ } else if (wins != 0) {
+ Log.w(TAG, "Reject ambiguous overlapping multi-ID artifact")
+ return null
+ }
+ }
+ return selected
+ }
+
+ private fun createPlan(apk: ModsApk): PlannedArtifact {
+ val comparisons = apk.identities.map { identity ->
+ val moduleId = checkNotNull(identity.moduleId)
+ val incoming = checkNotNull(identity.moduleVersion).toLong()
+ val installed = ChimeraConfigManager.findModuleByModuleId(moduleId)
+ Triple(incoming, installed?.moduleVersion?.toLongOrNull(), installed?.installedApkPath)
+ }
+ val hasMissing = comparisons.any { it.second == null }
+ val hasUpgrade = comparisons.any { (incoming, installed) ->
+ installed != null && incoming > installed
+ }
+ val allEqual = comparisons.all { (incoming, installed) ->
+ installed != null && incoming == installed
+ }
+ val decision = when {
+ hasMissing && comparisons.all { it.second == null } -> VersionDecision.INSTALL
+ hasMissing || hasUpgrade -> VersionDecision.OVERWRITE
+ allEqual -> VersionDecision.SKIP_SAME
+ else -> VersionDecision.SKIP_LOWER
+ }
+ return PlannedArtifact(
+ apk = apk,
+ moduleName = canonicalArtifactName(apk),
+ version = artifactVersion(apk),
+ decision = decision,
+ oldApkPaths = comparisons.mapNotNull { (incoming, installed, path) ->
+ path?.takeIf { installed == null || incoming >= installed }
+ }.toSet(),
+ )
+ }
+
+ private fun artifactVersion(apk: ModsApk): Long =
+ apk.identities.maxOf { checkNotNull(it.moduleVersion).toLong() }
+
+ private fun canonicalArtifactName(apk: ModsApk): String {
+ val names = apk.identities.map { identity ->
+ DynamicModuleRegistry.canonicalModuleName(checkNotNull(identity.moduleId))
+ }.distinct()
+ val raw = names.singleOrNull() ?: checkNotNull(apk.identities.first().moduleId)
+ return raw.replace(Regex("[^A-Za-z0-9_.-]"), "_").take(180)
+ }
+
+ private fun String.isValidRouteName(): Boolean =
+ isNotEmpty() && length <= 300 && all { it.isLetterOrDigit() || it == '.' || it == '$' || it == '_' }
+
+ private fun rejected(apks: List, failure: ModsImportFailure): ModsImportResult {
+ apks.forEach { apk ->
+ Log.w(TAG, "Import ${apk.identities.mapNotNull { it.moduleId }} failure=$failure")
+ }
+ return ModsImportResult(rejected = apks.size, failure = failure)
+ }
+
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModuleImportActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModuleImportActivity.kt
new file mode 100644
index 0000000000..60b6e6fe64
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModuleImportActivity.kt
@@ -0,0 +1,141 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.microg.gms.moduleinstall.dynamicmodule
+
+import android.content.ClipData
+import android.app.PendingIntent
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.google.android.chimera.component.ModuleDownloadActivity
+import com.google.android.chimera.component.ModuleImportCompletionReceiver
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.gms.R
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Imports a module from an externally opened `.mods` file, an Android share, or a Uri returned by
+ * the in-app SAF picker. Only the content-validated `.mods` container format is supported.
+ *
+ * Transparent activity: the import runs in the background and the result is reported only via Toast. Trust is
+ * established per-apk by the importer (each module apk must be Google-signed), so no confirmation dialog is
+ * shown here. The activity declares configChanges in the manifest so a rotation does not recreate it and
+ * cancel the in-flight install mid-loop.
+ */
+class ModuleImportActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (!DynamicModuleSettings.isAvailable(this)) {
+ val message = if (DynamicModuleSettings.isRuntimeSupported()) {
+ R.string.dynamicmodule_import_disabled
+ } else {
+ R.string.dynamicmodule_unsupported_android_version
+ }
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ setResult(RESULT_CANCELED)
+ finish()
+ return
+ }
+ val uri = resolveInputUri()
+ if (uri == null) {
+ Toast.makeText(this, R.string.dynamicmodule_import_no_file, Toast.LENGTH_SHORT).show()
+ setResult(RESULT_CANCELED)
+ finish()
+ return
+ }
+ startImport(uri)
+ }
+
+ /** ACTION_VIEW carries the Uri in data; ACTION_SEND carries it in EXTRA_STREAM. */
+ @Suppress("DEPRECATION")
+ private fun resolveInputUri(): Uri? = when (intent?.action) {
+ Intent.ACTION_SEND -> intent?.extractFileUri()
+ else -> intent?.extractFileUri() ?: intent?.data
+ }
+
+ private fun Intent.extractFileUri(): Uri? = getParcelableExtra(Intent.EXTRA_STREAM) as? Uri
+ ?: (clipData?.extractFirstUri() ?: data)
+
+ private fun ClipData.extractFirstUri(): Uri? = when {
+ itemCount > 0 -> getItemAt(0).uri
+ else -> null
+ }
+
+ @SuppressLint("StringFormatInvalid")
+ private fun startImport(uri: Uri) {
+ lifecycleScope.launch {
+ val importResult = withContext(Dispatchers.IO) {
+ ModsImporter.importFrom(this@ModuleImportActivity, uri)
+ }
+ val result = if (importResult.accepted) RESULT_OK else RESULT_CANCELED
+ val message = when (importResult.failure) {
+ ModsImportFailure.UNAVAILABLE -> if (DynamicModuleSettings.isRuntimeSupported()) {
+ getString(R.string.dynamicmodule_import_disabled)
+ } else {
+ getString(R.string.dynamicmodule_unsupported_android_version)
+ }
+
+ ModsImportFailure.SOURCE_UNAVAILABLE,
+ ModsImportFailure.INVALID_CONTAINER -> getString(R.string.dynamicmodule_import_no_file)
+
+ else -> getString(
+ R.string.dynamicmodule_import_bundle_summary,
+ importResult.installed,
+ importResult.skipped,
+ importResult.rejected,
+ )
+ }
+ Toast.makeText(this@ModuleImportActivity, message, Toast.LENGTH_LONG).show()
+ sendImportResultCallback(result, importResult)
+ if (result == RESULT_OK) {
+ // This is only a wake-up hint for a pending module request. Its waiting page reloads
+ // Chimera configuration and verifies its own requested feature before continuing.
+ sendBroadcast(
+ Intent(this@ModuleImportActivity, ModuleImportCompletionReceiver::class.java)
+ .setAction(ModuleDownloadActivity.ACTION_MODULE_IMPORT_COMPLETED)
+ )
+ }
+ setResult(result)
+ finish()
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun sendImportResultCallback(resultCode: Int, importResult: ModsImportResult) {
+ val callback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(EXTRA_IMPORT_RESULT_CALLBACK, PendingIntent::class.java)
+ } else {
+ intent.getParcelableExtra(EXTRA_IMPORT_RESULT_CALLBACK)
+ } ?: return
+ val resultIntent = Intent().apply {
+ putExtra(EXTRA_IMPORT_ACCEPTED, importResult.accepted)
+ putExtra(EXTRA_IMPORT_FAILURE, importResult.failure?.name)
+ }
+ runCatching {
+ callback.send(this, resultCode, resultIntent)
+ }.onSuccess {
+ Log.i(TAG, "Import result callback sent accepted=${importResult.accepted}")
+ }.onFailure {
+ Log.w(TAG, "Unable to send import result callback", it)
+ }
+ }
+
+ private companion object {
+ const val TAG = "GmsModule/Import"
+ const val EXTRA_IMPORT_RESULT_CALLBACK = "MODULE_IMPORT_RESULT_CALLBACK"
+ const val EXTRA_IMPORT_ACCEPTED = "MODULE_IMPORT_ACCEPTED"
+ const val EXTRA_IMPORT_FAILURE = "MODULE_IMPORT_FAILURE"
+ }
+
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/DynamicModuleManagerFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/DynamicModuleManagerFragment.kt
new file mode 100644
index 0000000000..806994cd69
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/DynamicModuleManagerFragment.kt
@@ -0,0 +1,195 @@
+/*
+ * SPDX-FileCopyrightText: 2026, microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.ui
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.core.net.toUri
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.TwoStatePreference
+import com.google.android.chimera.config.ChimeraConfigManager
+import com.google.android.chimera.config.ChimeraModuleBootstrap
+import com.google.android.chimera.config.DynamicModuleSettings
+import com.google.android.chimera.config.InstalledModuleStatus
+import com.google.android.chimera.config.ModuleCapabilityStatus
+import com.google.android.gms.R
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.microg.gms.moduleinstall.dynamicmodule.ModuleImportActivity
+
+/**
+ * Dynamic module management screen: enable toggle + .mods import entry + list of imported modules.
+ * The list only shows modules that are actually imported -- the data source is the chimera_manifest.pb
+ * persisted by ChimeraConfigManager (written when an import is flushed to disk, read reliably across
+ * processes via reload), rather than enumerating a hardcoded fixed module table.
+ */
+class DynamicModuleManagerFragment : PreferenceFragmentCompat() {
+ private lateinit var dynamicModuleEnabled: TwoStatePreference
+ private lateinit var modules: PreferenceCategory
+ private lateinit var moduleNone: Preference
+ private lateinit var moduleImport: Preference
+ private lateinit var modsImport: ActivityResultLauncher
+ private var moduleRefreshJob: Job? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ modsImport = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ if (uri != null) {
+ startActivity(Intent(requireContext(), ModuleImportActivity::class.java).apply {
+ data = uri
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ })
+ }
+ }
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ addPreferencesFromResource(R.xml.preferences_dynamicmodule)
+ modules = preferenceScreen.findPreference("prefcat_dynamicmodule_installed") ?: return
+ moduleNone = preferenceScreen.findPreference("pref_dynamicmodule_none") ?: return
+ moduleImport = preferenceScreen.findPreference("pref_dynamicmodule_import") ?: return
+ }
+
+ @SuppressLint("RestrictedApi")
+ override fun onBindPreferences() {
+ moduleImport.setOnPreferenceClickListener {
+ // "*/*": a content:// .mods rarely has a registered MIME type; the importer validates content.
+ modsImport.launch("*/*")
+ true
+ }
+ dynamicModuleEnabled = preferenceScreen.findPreference(PREF_DYNAMIC_MODULE_ENABLED) ?: return
+ dynamicModuleEnabled.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ if (newValue is Boolean) {
+ if (newValue && !DynamicModuleSettings.isRuntimeSupported()) {
+ Toast.makeText(
+ requireContext(),
+ R.string.dynamicmodule_unsupported_android_version,
+ Toast.LENGTH_SHORT,
+ ).show()
+ return@OnPreferenceChangeListener false
+ }
+ val appContext = requireContext().applicationContext
+ val updated = DynamicModuleSettings.setEnabled(appContext, newValue)
+ updateModules()
+ updated
+ } else {
+ false
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateModules()
+ }
+
+ override fun onDestroy() {
+ moduleRefreshJob?.cancel()
+ moduleRefreshJob = null
+ super.onDestroy()
+ }
+
+ private fun updateModules() {
+ moduleRefreshJob?.cancel()
+ val appContext = requireContext().applicationContext
+ val runtimeSupported = DynamicModuleSettings.isRuntimeSupported()
+ dynamicModuleEnabled.isChecked = DynamicModuleSettings.isEnabled(appContext)
+ dynamicModuleEnabled.isEnabled = runtimeSupported
+ moduleImport.isEnabled = runtimeSupported && dynamicModuleEnabled.isChecked
+ moduleRefreshJob = lifecycleScope.launch {
+ val installed = withContext(Dispatchers.IO) {
+ // The :ui process AppContext may not be initialized; without initializing first,
+ // getChimeraManifest returns an empty store and the list comes up empty.
+ ChimeraModuleBootstrap.ensureInitialized(appContext)
+ runCatching { ChimeraConfigManager.reload() }
+ ChimeraConfigManager.listInstalledModuleStatuses(appContext)
+ .filter { it.moduleName.isNotEmpty() && it.moduleName != "ROOT" }
+ .sortedBy { it.moduleName }
+ .also { Log.d(TAG, "updateModules: ${it.size} installed modules") }
+ }
+ if (!isResumed) return@launch
+ renderModules(appContext, installed)
+ }
+ }
+
+ private fun renderModules(context: Context, list: List) {
+ modules.removeAll()
+ modules.isVisible = true
+ moduleNone.isVisible = false
+ for (module in list) {
+ val name = module.moduleName
+ val pref = Preference(context).apply {
+ key = "pref_dynamicmodule_module_$name"
+ title = name
+ summary = context.getString(
+ R.string.dynamicmodule_version_fmt,
+ module.moduleVersion.ifEmpty { "?" },
+ context.getString(capabilityStatusString(module.capabilityStatus))
+ )
+ setOnPreferenceClickListener { confirmDelete(name); true }
+ }
+ modules.addPreference(pref)
+ }
+ if (modules.preferenceCount == 0) {
+ moduleNone.isVisible = true
+ modules.addPreference(moduleNone)
+ }
+ }
+
+ private fun capabilityStatusString(status: ModuleCapabilityStatus): Int = when (status) {
+ ModuleCapabilityStatus.LOADABLE -> R.string.dynamicmodule_state_loaded
+ ModuleCapabilityStatus.PARTIAL_COMPONENT_SUPPORT -> R.string.dynamicmodule_state_partial
+ ModuleCapabilityStatus.UNSUPPORTED_INITIALIZER -> R.string.dynamicmodule_state_unsupported_initializer
+ ModuleCapabilityStatus.UNVERIFIED_ARTIFACT -> R.string.dynamicmodule_state_unverified
+ }
+
+ private fun confirmDelete(moduleName: String) {
+ // Must use the Activity context (with AppCompat theme), not applicationContext, or AlertDialog crashes.
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.dynamicmodule_remove_action)
+ .setMessage(moduleName)
+ .setPositiveButton(R.string.dynamicmodule_remove_action) { _, _ -> removeModule(moduleName) }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ private fun removeModule(moduleName: String) {
+ val appContext = requireContext().applicationContext
+ lifecycleScope.launch {
+ val ok = withContext(Dispatchers.IO) {
+ runCatching {
+ appContext.contentResolver.call(
+ "content://com.google.android.gms.chimera".toUri(),
+ "removeModule", moduleName, null
+ )?.getBoolean("removed", false) ?: false
+ }.onFailure { Log.w(TAG, "removeModule IPC failed for $moduleName", it) }.getOrDefault(false)
+ }
+ Toast.makeText(
+ appContext,
+ if (ok) R.string.dynamicmodule_remove_done else R.string.dynamicmodule_remove_failed,
+ Toast.LENGTH_SHORT
+ ).show()
+ updateModules()
+ }
+ }
+
+ companion object {
+ private const val TAG = "DynamicModuleMgr"
+ const val PREF_DYNAMIC_MODULE_ENABLED = "pref_dynamicmodule_enabled"
+ }
+}
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt
index 70335535ce..22029e559d 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt
@@ -54,6 +54,10 @@ class SettingsFragment : ResourceSettingsFragment() {
findNavController().navigate(requireContext(), R.id.openWorkProfileSettings)
true
}
+ findPreference(PREF_DYNAMICMODULE)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ findNavController().navigate(requireContext(), R.id.openDynamicModuleManager)
+ true
+ }
findPreference(PREF_ABOUT)!!.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
@@ -138,6 +142,7 @@ class SettingsFragment : ResourceSettingsFragment() {
const val PREF_CHECKIN = "pref_checkin"
const val PREF_VENDING = "pref_vending"
const val PREF_WORK_PROFILE = "pref_work_profile"
+ const val PREF_DYNAMICMODULE = "pref_dynamicmodule"
const val PREF_ACCOUNTS = "pref_accounts"
}
diff --git a/play-services-core/src/main/res/drawable/ic_dynamicmodule.xml b/play-services-core/src/main/res/drawable/ic_dynamicmodule.xml
new file mode 100644
index 0000000000..a84978a309
--- /dev/null
+++ b/play-services-core/src/main/res/drawable/ic_dynamicmodule.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/play-services-core/src/main/res/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml
index b1b11dd439..2234fde196 100644
--- a/play-services-core/src/main/res/navigation/nav_settings.xml
+++ b/play-services-core/src/main/res/navigation/nav_settings.xml
@@ -35,6 +35,9 @@
+
@@ -203,6 +206,13 @@
android:name="org.microg.gms.ui.GoogleMoreFragment"
android:label="@string/gms_settings_name" />
+
+
+
+
蓝牙
混合
ID: %1$s
+ 动态模块
+ 动态模块管理
+ 可动态加载的模块(文档扫描、ML Kit 模型等)按需热加载。导入模块包(.mods)即可启用对应功能。
+ 启用动态模块
+ 已加载
+ 部分兼容
+ 不支持的模块 API
+ 模块文件验证失败
+ 模块已移除
+ 删除
+ v%1$s · %2$s
+ 删除模块失败
+ 导入模块文件(.mods)
+ 选择本地 .mods 文件手动导入模块
+ 未提供模块文件
+ 请先启用动态模块,再导入模块包。
+ 动态模块需要 Android 11 或更高版本。
+ 模块:%1$d 个已安装 · %2$d 个已跳过 · %3$d 个已拒绝
diff --git a/play-services-core/src/main/res/values-zh-rTW/strings.xml b/play-services-core/src/main/res/values-zh-rTW/strings.xml
index bd9ff44bd3..127fce34de 100644
--- a/play-services-core/src/main/res/values-zh-rTW/strings.xml
+++ b/play-services-core/src/main/res/values-zh-rTW/strings.xml
@@ -422,4 +422,22 @@
藍牙
混合
ID: %1$s
+ 動態模組
+ 動態模組管理
+ 可動態載入的模組(文件掃描、ML Kit 模型等)依需求熱載入。匯入模組套件(.mods)即可啟用對應功能。
+ 啟用動態模組
+ 已載入
+ 部分相容
+ 不支援的模組 API
+ 模組檔案驗證失敗
+ 模組已移除
+ 刪除
+ v%1$s · %2$s
+ 刪除模組失敗
+ 匯入模組檔案(.mods)
+ 選擇本機 .mods 檔案手動匯入模組
+ 未提供模組檔案
+ 請先啟用動態模組,再匯入模組套件。
+ 動態模組需要 Android 11 或以上版本。
+ 模組:%1$d 個已安裝 · %2$d 個已跳過 · %3$d 個已拒絕
diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml
index 48d05db18b..aaf207eecb 100644
--- a/play-services-core/src/main/res/values/strings.xml
+++ b/play-services-core/src/main/res/values/strings.xml
@@ -406,6 +406,25 @@ Please set up a password, PIN, or pattern lock screen."
Permanently deleting your data for %1$s will remove your scores, progress (saved games), and game settings in Google Play Games.
Hey there, %1$s
+ Dynamic modules
+ Dynamic Module Management
+ Dynamically loadable modules (document scanner, ML Kit models, etc.) are hot-loaded on demand. Import a module bundle (.mods) to enable its features.
+ Enable dynamic modules
+ Loaded
+ Partial compatibility
+ Unsupported module API
+ Artifact verification failed
+ Module removed
+ Delete
+ v%1$s · %2$s
+ Failed to remove module
+ Import module file (.mods)
+ Manually import a module by selecting a local .mods file
+ No module file provided
+ Enable dynamic modules before importing a module bundle.
+ Dynamic modules require Android 11 or newer.
+ Modules: %1$d installed · %2$d skipped · %3$d rejected
+
Family
Retry
Content loading failed
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
index cf9fe6d9f6..30ff0e85aa 100644
--- a/play-services-core/src/main/res/xml/file_provider_paths.xml
+++ b/play-services-core/src/main/res/xml/file_provider_paths.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/play-services-core/src/main/res/xml/preferences_dynamicmodule.xml b/play-services-core/src/main/res/xml/preferences_dynamicmodule.xml
new file mode 100644
index 0000000000..bf0e60430c
--- /dev/null
+++ b/play-services-core/src/main/res/xml/preferences_dynamicmodule.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/play-services-core/src/main/res/xml/preferences_start.xml b/play-services-core/src/main/res/xml/preferences_start.xml
index eb45098992..fbe70de9bf 100644
--- a/play-services-core/src/main/res/xml/preferences_start.xml
+++ b/play-services-core/src/main/res/xml/preferences_start.xml
@@ -52,6 +52,10 @@
android:icon="@drawable/ic_work"
android:key="pref_work_profile"
android:title="@string/service_name_work_profile" />
+