From 7bc407073d4799279580d190219a6385256aa740 Mon Sep 17 00:00:00 2001 From: Davinci9196 Date: Fri, 14 Aug 2026 11:32:22 +0800 Subject: [PATCH 1/4] Add Chimera module loading for ML Kit document scanning --- .../internal/IFlagUpdateListener.aidl | 10 + .../internal/IGetStorageInfoCallbacks.aidl | 11 + .../core/package/src/main/AndroidManifest.xml | 8 - .../gms/moduleinstall/ModuleInstallService.kt | 65 - .../microg/gms/common/KnownGooglePackages.kt | 8 + .../microg/gms/settings/SettingsContract.kt | 11 + .../microg/gms/settings/SettingsProvider.kt | 22 + .../gms/common/net/ISocketFactoryCreator.aidl | 13 + .../android/gms/dynamite/IDynamiteLoader.aidl | 12 +- .../android/gms/dynamite/DynamiteModule.java | 212 +- play-services-chimera-core/build.gradle | 6 + .../google/android/chimera/BoundService.kt | 83 + .../chimera/DynamicBroadcastReceiver.kt | 13 + .../android/chimera/android/Activity.kt | 1380 ++++++++++ .../chimera/android/ActivityProxyWrapper.kt | 2324 +++++++++++++++++ .../chimera/android/ChimeraActivityProxy.kt | 524 ++++ .../android/chimera/android/IActivityProxy.kt | 663 +++++ .../chimera/android/IChimeraActivityProxy.kt | 12 + .../chimera/annotation/ChimeraApiVersion.java | 17 + .../chimera/component/BaseActivityProxy.kt | 2193 ++++++++++++++++ .../component/ChimeraComponentProxy.kt | 19 + .../component/ChimeraFallbackActImpl.kt | 77 + .../chimera/component/ChimeraFileApk.kt | 130 + .../component/ChimeraModuleContextProvider.kt | 14 + .../component/ChimeraPermissionActImpl.kt | 70 + .../chimera/component/ChimeraProxyCallback.kt | 11 + .../android/chimera/component/ContainerApk.kt | 66 + .../component/ModuleDownloadActivity.kt | 572 ++++ .../ModuleImportCompletionReceiver.kt | 55 + .../component/PendingModuleRequestStore.kt | 170 ++ .../config/ChimeraApkManifestReader.kt | 106 + .../chimera/config/ChimeraConfigManager.kt | 546 ++++ .../chimera/config/ChimeraModuleBootstrap.kt | 81 + .../config/ChimeraModuleCapabilities.kt | 95 + .../chimera/config/ChimeraModuleInfoImpl.kt | 24 + .../chimera/config/ChimeraModuleManager.kt | 336 +++ .../android/chimera/config/ChimeraStorage.kt | 208 ++ .../chimera/config/DynamicModuleSettings.kt | 62 + .../chimera/config/FeatureCheckUtils.kt | 163 ++ .../chimera/config/InvalidConfigException.kt | 16 + .../chimera/config/ModuleDownloadRegistry.kt | 383 +++ .../android/chimera/config/ModuleManager.kt | 292 +++ .../chimera/config/registry/ApkRegistry.kt | 38 + .../chimera/config/registry/ApkType.kt | 10 + .../config/registry/ContainerRouteRegistry.kt | 25 + .../config/registry/DynamicModuleRegistry.kt | 79 + .../config/registry/FeatureConfigRegistry.kt | 23 + .../android/chimera/container/ModuleApi.kt | 40 + .../chimera/context/ContextThemeWrapper.kt | 86 + .../chimera/context/GmsContextWrapper.kt | 46 + .../android/chimera/context/ModuleContext.kt | 427 +++ .../chimera/context/ModuleContextRef.kt | 16 + .../chimera/context/ModulePackageManager.kt | 53 + .../android/chimera/loader/ApkInfoKey.kt | 50 + .../chimera/loader/BaseFileModuleApk.kt | 70 + .../chimera/loader/ChimeraApkLoader.kt | 157 ++ .../chimera/loader/ChimeraModuleApk.kt | 72 + .../chimera/loader/ChimeraModuleLdr.kt | 326 +++ .../android/chimera/util/ChimeraResource.kt | 100 + .../chimera/util/ChimeraViewCreator.kt | 51 + .../android/gms/chimera/GmsApiService.kt | 102 + .../gms/chimera/PersistentApiService.kt | 21 + .../PersistentDirectBootAwareApiService.kt | 19 + .../chimera/container/DynamiteModuleApi.kt | 57 + .../gms/chimera/container/GmsModuleApi.kt | 59 + .../android/gms/common/app/AppContext.kt | 22 + .../gms/common/app/BaseApplicationContext.kt | 70 + .../gms/common/app/GCoreApplicationContext.kt | 14 + .../threads/internal/GlobalExecutorsImpl.kt | 54 + .../docscan/ui/DocumentScanningActivity.kt | 11 + .../src/main/res/values-zh-rCN/strings.xml | 34 + .../src/main/res/values-zh-rTW/strings.xml | 34 + .../src/main/res/values/strings.xml | 38 + play-services-chimeraresources/build.gradle | 22 + .../src/main/res/values/ids.xml | 3 + .../main/proto/chimera/chimeramanifest.proto | 80 + .../src/main/proto/chimera/modulefile.proto | 14 + .../main/proto/chimera/modulemanager.proto | 20 + play-services-core/build.gradle | 1 + .../src/huawei/AndroidManifest.xml | 3 + .../src/main/AndroidManifest.xml | 101 + .../gms/chimera/DynamiteContextFactory.java | 151 +- .../chimera/container/DynamiteContext.java | 23 +- .../chimera/container/DynamiteLoaderImpl.java | 266 +- .../ModuleDescriptor.java | 14 + .../ModuleDescriptor.java | 14 + .../mlkit_docscan_stain/ModuleDescriptor.java | 14 + .../mlkit_docscan_ui/ModuleDescriptor.java | 15 + .../microg/gms/ui/MainSettingsActivity.java | 23 + .../gms/chimera/ChimeraModuleRemover.kt | 67 + .../org/microg/gms/chimera/ServiceProvider.kt | 228 +- .../gms/moduleinstall/ModuleInstallService.kt | 328 +++ .../gms/moduleinstall/ModuleInstaller.kt | 151 ++ .../dynamicmodule/ApkSignatureVerifier.kt | 95 + .../dynamicmodule/ModsContainer.kt | 117 + .../dynamicmodule/ModsImporter.kt | 336 +++ .../dynamicmodule/ModuleImportActivity.kt | 141 + .../gms/ui/DynamicModuleManagerFragment.kt | 195 ++ .../org/microg/gms/ui/SettingsFragment.kt | 5 + .../main/res/drawable/ic_dynamicmodule.xml | 16 + .../src/main/res/navigation/nav_settings.xml | 10 + .../src/main/res/values-zh-rCN/strings.xml | 18 + .../src/main/res/values-zh-rTW/strings.xml | 18 + .../src/main/res/values/strings.xml | 19 + .../src/main/res/xml/file_provider_paths.xml | 1 + .../res/xml/preferences_dynamicmodule.xml | 36 + .../src/main/res/xml/preferences_start.xml | 4 + settings.gradle | 1 + 108 files changed, 15642 insertions(+), 195 deletions(-) create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IFlagUpdateListener.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IGetStorageInfoCallbacks.aidl delete mode 100644 play-services-base/core/package/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt create mode 100644 play-services-basement/src/main/aidl/com/google/android/gms/common/net/ISocketFactoryCreator.aidl create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/BoundService.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/DynamicBroadcastReceiver.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/android/Activity.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/android/ActivityProxyWrapper.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/android/ChimeraActivityProxy.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/android/IActivityProxy.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/android/IChimeraActivityProxy.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/annotation/ChimeraApiVersion.java create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/BaseActivityProxy.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraComponentProxy.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFallbackActImpl.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraFileApk.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraModuleContextProvider.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraPermissionActImpl.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ChimeraProxyCallback.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ContainerApk.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleDownloadActivity.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/ModuleImportCompletionReceiver.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/component/PendingModuleRequestStore.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraApkManifestReader.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraConfigManager.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleBootstrap.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleCapabilities.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleInfoImpl.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraModuleManager.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ChimeraStorage.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/DynamicModuleSettings.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/FeatureCheckUtils.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/InvalidConfigException.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleDownloadRegistry.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/ModuleManager.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkRegistry.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ApkType.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/ContainerRouteRegistry.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/DynamicModuleRegistry.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/config/registry/FeatureConfigRegistry.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/container/ModuleApi.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/context/ContextThemeWrapper.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/context/GmsContextWrapper.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContext.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModuleContextRef.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/context/ModulePackageManager.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ApkInfoKey.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/loader/BaseFileModuleApk.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraApkLoader.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleApk.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/loader/ChimeraModuleLdr.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraResource.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/chimera/util/ChimeraViewCreator.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/chimera/GmsApiService.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentApiService.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/chimera/PersistentDirectBootAwareApiService.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleApi.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/chimera/container/GmsModuleApi.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/common/app/AppContext.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/common/app/BaseApplicationContext.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/common/app/GCoreApplicationContext.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/common/threads/internal/GlobalExecutorsImpl.kt create mode 100644 play-services-chimera-core/src/main/java/com/google/android/gms/mlkit/docscan/ui/DocumentScanningActivity.kt create mode 100644 play-services-chimera-core/src/main/res/values-zh-rCN/strings.xml create mode 100644 play-services-chimera-core/src/main/res/values-zh-rTW/strings.xml create mode 100644 play-services-chimera-core/src/main/res/values/strings.xml create mode 100644 play-services-chimeraresources/build.gradle create mode 100644 play-services-chimeraresources/src/main/res/values/ids.xml create mode 100644 play-services-core-proto/src/main/proto/chimera/chimeramanifest.proto create mode 100644 play-services-core-proto/src/main/proto/chimera/modulefile.proto create mode 100644 play-services-core-proto/src/main/proto/chimera/modulemanager.proto create mode 100644 play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_detect/ModuleDescriptor.java create mode 100644 play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_enhance/ModuleDescriptor.java create mode 100644 play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_stain/ModuleDescriptor.java create mode 100644 play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/mlkit_docscan_ui/ModuleDescriptor.java create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/chimera/ChimeraModuleRemover.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstallService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/ModuleInstaller.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ApkSignatureVerifier.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsContainer.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsImporter.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModuleImportActivity.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/ui/DynamicModuleManagerFragment.kt create mode 100644 play-services-core/src/main/res/drawable/ic_dynamicmodule.xml create mode 100644 play-services-core/src/main/res/xml/preferences_dynamicmodule.xml 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/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..1671e85ebc --- /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 = "ChimeraApkManifestReader" + 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..9967e2af96 --- /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.putIfAbsent(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..b7e34fcb0d --- /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.removeIf { 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..c63423c09b --- /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.removeIf { 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 + "-"; + synchronized (sContextCache) { + sContextCache.keySet().removeIf(key -> key.startsWith(prefix)); + } + synchronized (sClassLoaderCache) { + sClassLoaderCache.keySet().removeIf(key -> key.startsWith(prefix)); + } + Log.d(TAG, "Cleared Dynamite caches for moduleId: " + moduleId); + } + + /** + * 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 +183,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..cd90808ff1 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/moduleinstall/dynamicmodule/ModsImporter.kt @@ -0,0 +1,336 @@ +/* + * 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 { + val previous = routes.putIfAbsent(containerName, moduleId) + return previous == null || previous == 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" /> + Date: Fri, 14 Aug 2026 11:55:50 +0800 Subject: [PATCH 2/4] update --- play-services-chimera-core/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) 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 --> + + From 45926730459d4f89667ff70bfd7259f479dfaddc Mon Sep 17 00:00:00 2001 From: Davinci9196 Date: Fri, 14 Aug 2026 13:43:45 +0800 Subject: [PATCH 3/4] update --- .../android/chimera/loader/ChimeraApkLoader.kt | 2 +- .../android/chimera/loader/ChimeraModuleLdr.kt | 2 +- .../gms/chimera/DynamiteContextFactory.java | 18 ++++++++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) 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 index b7e34fcb0d..5e16cb4ef0 100644 --- 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 @@ -133,7 +133,7 @@ object ChimeraApkLoader { fun clearModuleCaches(moduleName: String? = null, apkPath: String? = null) { if (moduleName.isNullOrEmpty() && apkPath.isNullOrEmpty()) return - moduleContextCache.entries.removeIf { entry -> + moduleContextCache.entries.removeAll { entry -> val info = entry.key.apkInfo (!moduleName.isNullOrEmpty() && info.moduleName == moduleName) || (!apkPath.isNullOrEmpty() && info.apkPath == apkPath) 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 index c63423c09b..c56613c560 100644 --- 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 @@ -115,7 +115,7 @@ object ChimeraModuleLdr { fun clearModuleCache(moduleId: String? = null, moduleName: String? = null, apkPath: String? = null) { if (moduleId.isNullOrEmpty() && moduleName.isNullOrEmpty() && apkPath.isNullOrEmpty()) return - loaderModuleContext.entries.removeIf { entry -> + loaderModuleContext.entries.removeAll { entry -> val ref = entry.value (!moduleId.isNullOrEmpty() && entry.key == moduleId) || (!moduleName.isNullOrEmpty() && ref.moduleName == moduleName) || diff --git a/play-services-core/src/main/java/com/google/android/gms/chimera/DynamiteContextFactory.java b/play-services-core/src/main/java/com/google/android/gms/chimera/DynamiteContextFactory.java index 3470385245..7563852b1a 100644 --- a/play-services-core/src/main/java/com/google/android/gms/chimera/DynamiteContextFactory.java +++ b/play-services-core/src/main/java/com/google/android/gms/chimera/DynamiteContextFactory.java @@ -27,6 +27,7 @@ import java.io.File; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.WeakHashMap; @@ -41,15 +42,20 @@ public class DynamiteContextFactory { public static void clearCacheForModule(String moduleId) { if (moduleId == null || moduleId.isEmpty()) return; String prefix = moduleId + "-"; - synchronized (sContextCache) { - sContextCache.keySet().removeIf(key -> key.startsWith(prefix)); - } - synchronized (sClassLoaderCache) { - sClassLoaderCache.keySet().removeIf(key -> key.startsWith(prefix)); - } + 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. From 1cb7e3f2487e8334773a08b02ff715f51738d266 Mon Sep 17 00:00:00 2001 From: Davinci9196 Date: Fri, 14 Aug 2026 14:16:19 +0800 Subject: [PATCH 4/4] update --- .../google/android/chimera/config/ChimeraApkManifestReader.kt | 2 +- .../com/google/android/chimera/config/ChimeraModuleManager.kt | 2 +- .../org/microg/gms/moduleinstall/dynamicmodule/ModsImporter.kt | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) 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 index 1671e85ebc..200a527f38 100644 --- 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 @@ -26,7 +26,7 @@ data class ChimeraApkIdentity( * same length checks and malformed-APK behavior. */ object ChimeraApkManifestReader { - private const val TAG = "ChimeraApkManifestReader" +private const val TAG = "ChimeraManifestReader" private const val ENTRY_CHIMERA_MANIFEST = "assets/ChimeraManifest.pb" private const val MAX_CHIMERA_MANIFEST_BYTES = 1024L * 1024 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 index 9967e2af96..f373719220 100644 --- 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 @@ -137,7 +137,7 @@ class ChimeraModuleManager( for (chimeraModule in ChimeraConfigManager.getRegisteredModules()) { val moduleId = chimeraModule.moduleId?.takeIf { it.isNotEmpty() } ?: continue val dynamicModule = toDynamicModule(moduleId, chimeraModule) - modules.putIfAbsent(dynamicModule.primaryModuleId, dynamicModule to chimeraModule) + modules.getOrPut(dynamicModule.primaryModuleId) { dynamicModule to chimeraModule } } return modules.values.map { (dynamicModule, chimeraModule) -> toModuleInfo(dynamicModule, chimeraModule, null) 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 index cd90808ff1..f6ba494ecd 100644 --- 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 @@ -250,8 +250,7 @@ internal object ModsImporter { } private fun validateRoute(routes: MutableMap, containerName: String, moduleId: String): Boolean { - val previous = routes.putIfAbsent(containerName, moduleId) - return previous == null || previous == moduleId + return routes.getOrPut(containerName) { moduleId } == moduleId } /** Selects one whole APK per duplicated module ID and rejects ambiguous multi-ID overlap. */