├── config-holder ├── build.gradle.kts ├── .gitignore ├── app │ ├── res │ │ └── values │ │ │ └── strings.xml │ ├── AndroidManifest.xml │ └── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── gradlew.bat └── gradlew ├── src ├── com │ ├── google │ │ └── android │ │ │ └── gms │ │ │ ├── common │ │ │ ├── api │ │ │ │ ├── Status.aidl │ │ │ │ ├── Result.java │ │ │ │ ├── internal │ │ │ │ │ └── IStatusCallback.aidl │ │ │ │ ├── CommonStatusCodes.java │ │ │ │ └── Status.java │ │ │ ├── ConnectionResult.aidl │ │ │ ├── internal │ │ │ │ └── ICancelToken.aidl │ │ │ └── ConnectionResult.java │ │ │ ├── location │ │ │ ├── LocationRequest.aidl │ │ │ ├── LocationResult.aidl │ │ │ ├── LastLocationRequest.aidl │ │ │ ├── LocationAvailability.aidl │ │ │ ├── CurrentLocationRequest.aidl │ │ │ ├── LocationSettingsResult.aidl │ │ │ ├── LocationSettingsRequest.aidl │ │ │ ├── internal │ │ │ │ ├── LocationReceiver.aidl │ │ │ │ ├── LocationRequestInternal.aidl │ │ │ │ ├── LocationRequestUpdateData.aidl │ │ │ │ ├── FusedLocationProviderResult.aidl │ │ │ │ ├── IBooleanStatusCallback.aidl │ │ │ │ ├── ISettingsCallbacks.aidl │ │ │ │ ├── ILocationStatusCallback.aidl │ │ │ │ ├── IFusedLocationProviderCallback.aidl │ │ │ │ ├── ILocationAvailabilityStatusCallback.aidl │ │ │ │ ├── FusedLocationProviderResult.java │ │ │ │ ├── LocationReceiver.java │ │ │ │ ├── LocationRequestInternal.java │ │ │ │ ├── LocationRequestUpdateData.java │ │ │ │ └── IGoogleLocationManagerService.aidl │ │ │ ├── LocationAvailabilityRequest.aidl │ │ │ ├── ILocationListener.aidl │ │ │ ├── ILocationCallback.aidl │ │ │ ├── LocationAvailabilityRequest.java │ │ │ ├── LocationResult.java │ │ │ ├── LastLocationRequest.java │ │ │ ├── LocationSettingsRequest.java │ │ │ ├── LocationSettingsResult.java │ │ │ ├── CurrentLocationRequest.java │ │ │ ├── LocationSettingsStates.java │ │ │ ├── LocationAvailability.java │ │ │ └── LocationRequest.java │ │ │ └── RoSafeParcelable.java │ └── android │ │ └── server │ │ └── location │ │ └── fudger │ │ └── LocationFudger.java └── app │ └── grapheneos │ └── gmscompat │ ├── Const.java │ ├── MainActivity.kt │ ├── BinderDefSupplier.kt │ ├── AbsContentProvider.java │ ├── ConfigUpdateReceiver.java │ ├── location │ ├── StubResolutionActivity.java │ ├── Client.kt │ ├── Listeners.kt │ ├── GLocationForwarder.kt │ ├── OsLocationProvider.kt │ └── OsLocationListener.kt │ ├── RpcProvider.kt │ ├── App.java │ ├── NotableInterface.kt │ ├── TempServiceBinding.java │ ├── PendingActionReceiver.java │ ├── PersistentFgService.java │ ├── BinderClientOfGmsCore2Gca.kt │ ├── BinderDefs.kt │ ├── PrivSettings.kt │ ├── Utils.kt │ └── Notifications.kt ├── lib ├── res │ └── values │ │ └── strings.xml ├── src │ ├── com │ │ └── google │ │ │ └── android │ │ │ └── play │ │ │ └── core │ │ │ └── integrity │ │ │ └── protocol │ │ │ ├── IIntegrityServiceCallback.aidl │ │ │ ├── IExpressIntegrityServiceCallback.aidl │ │ │ ├── IIntegrityService.aidl │ │ │ └── IExpressIntegrityService.aidl │ └── app │ │ └── grapheneos │ │ └── gmscompat │ │ └── lib │ │ ├── util │ │ ├── BinderUtils.java │ │ ├── ServiceConnectionWrapper.java │ │ ├── Certs.java │ │ └── BaseIContentProvider.java │ │ ├── sysservice │ │ ├── client │ │ │ ├── ClientServiceOverridesRegistry.java │ │ │ ├── GclPackageManager.java │ │ │ ├── ShimGmsFontProvider.java │ │ │ └── GclActivityManager.java │ │ ├── SystemServiceOverridesRegistry.java │ │ └── GmcActivityManager.java │ │ ├── playintegrity │ │ ├── ClassicPlayIntegrityServiceWrapper.java │ │ ├── StandardPlayIntegrityServiceWrapper.java │ │ ├── PlayIntegrityUtils.java │ │ └── PlayIntegrityServiceWrapper.java │ │ └── GmsCompatLibImpl.java ├── Android.bp ├── AndroidManifest.xml └── build.sh ├── etc ├── sysconfig │ └── app.grapheneos.gmscompat.xml └── default-permissions │ └── app.grapheneos.gmscompat.xml ├── jarjar-rules.txt ├── .github ├── workflows │ ├── validate-gradle-wrapper.yml │ └── build.yml └── dependabot.yml ├── res ├── values │ ├── arrays.xml │ └── strings.xml ├── layout │ └── main_activity.xml └── drawable │ ├── ic_configuration_required.xml │ ├── ic_crash_report.xml │ ├── ic_pending_action.xml │ ├── ic_screen_share.xml │ └── ic_info.xml ├── LICENSE ├── Android.bp ├── AndroidManifest.xml └── gmscompat_config /config-holder/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") version "8.13.1" apply false 3 | } 4 | -------------------------------------------------------------------------------- /config-holder/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /app/build 3 | local.properties 4 | keystore.properties 5 | *.keystore 6 | /releases 7 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/api/Status.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common.api; 2 | 3 | parcelable Status; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/ConnectionResult.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common; 2 | 3 | parcelable ConnectionResult; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationRequest.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable LocationRequest; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationResult.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable LocationResult; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LastLocationRequest.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable LastLocationRequest; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationAvailability.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable LocationAvailability; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/CurrentLocationRequest.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable CurrentLocationRequest; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationSettingsResult.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable LocationSettingsResult; 4 | -------------------------------------------------------------------------------- /lib/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GmsCompatLib 4 | 5 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationSettingsRequest.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable LocationSettingsRequest; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/LocationReceiver.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | parcelable LocationReceiver; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationAvailabilityRequest.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | parcelable LocationAvailabilityRequest; 4 | -------------------------------------------------------------------------------- /config-holder/app/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GmsCompatConfig 4 | 5 | -------------------------------------------------------------------------------- /config-holder/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GrapheneOS/platform_packages_apps_GmsCompat/HEAD/config-holder/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/LocationRequestInternal.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | parcelable LocationRequestInternal; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/LocationRequestUpdateData.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | parcelable LocationRequestUpdateData; 4 | -------------------------------------------------------------------------------- /etc/sysconfig/app.grapheneos.gmscompat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/FusedLocationProviderResult.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | parcelable FusedLocationProviderResult; 4 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/internal/ICancelToken.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common.internal; 2 | 3 | interface ICancelToken { 4 | oneway void cancel() = 1; 5 | } 6 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/ILocationListener.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.location.Location; 4 | 5 | interface ILocationListener { 6 | void onLocationChanged(in Location location); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/Const.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat; 2 | 3 | import android.app.compat.gms.GmsCompat; 4 | 5 | public interface Const { 6 | boolean ENABLE_LOGGING = true; 7 | boolean IS_DEV_BUILD = GmsCompat.isDevBuild(); 8 | } 9 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/api/Result.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common.api; 2 | 3 | // https://developers.google.com/android/reference/com/google/android/gms/common/api/Result 4 | public interface Result { 5 | Status getStatus(); 6 | } 7 | -------------------------------------------------------------------------------- /jarjar-rules.txt: -------------------------------------------------------------------------------- 1 | # Needed to avoid conflicts with the framework safeparcel instance. Using it directly is possible, 2 | # but that requires extensive changes to AppSearch module 3 | rule android.app.appsearch.safeparcel.* app.grapheneos.gmscompat.safeparcel.@1 4 | -------------------------------------------------------------------------------- /lib/src/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.play.core.integrity.protocol; 2 | 3 | import android.os.Bundle; 4 | 5 | interface IIntegrityServiceCallback { 6 | oneway void onResult(in Bundle result) = 1; 7 | } 8 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/IBooleanStatusCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import com.google.android.gms.common.api.Status; 4 | 5 | interface IBooleanStatusCallback { 6 | oneway void onResult(in Status status, boolean result) = 0; 7 | } 8 | -------------------------------------------------------------------------------- /config-holder/app/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /etc/default-permissions/app.grapheneos.gmscompat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/ISettingsCallbacks.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import com.google.android.gms.location.LocationSettingsResult; 4 | 5 | interface ISettingsCallbacks { 6 | void onLocationSettingsResult(in LocationSettingsResult result); 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/validate-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Validate Gradle Wrapper 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | validation: 7 | name: Validation 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v5 11 | - uses: gradle/actions/wrapper-validation@v5 12 | -------------------------------------------------------------------------------- /res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7ce83c1b71f3d572fed04c8d40c5cb10ff75e6d87d9df6fbd53f0468c2905053 5 | 5f2391277b1dbd489000467e4c2fa6af802430080457dce2f618992e9dfb5402 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/src/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.play.core.integrity.protocol; 2 | 3 | import android.os.Bundle; 4 | 5 | interface IExpressIntegrityServiceCallback { 6 | oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2; 7 | } 8 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/api/internal/IStatusCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common.api.internal; 2 | 3 | import android.location.Location; 4 | 5 | import com.google.android.gms.common.api.Status; 6 | 7 | interface IStatusCallback { 8 | oneway void onCompletion(in Status status); 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | target-branch: 15 8 | - package-ecosystem: gradle 9 | directory: "/config-holder" 10 | schedule: 11 | interval: daily 12 | target-branch: 15 13 | -------------------------------------------------------------------------------- /res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/ILocationStatusCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import android.location.Location; 4 | 5 | import com.google.android.gms.common.api.Status; 6 | 7 | interface ILocationStatusCallback { 8 | void onResult(in Status status, in Location location); 9 | } 10 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/IFusedLocationProviderCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import com.google.android.gms.location.internal.FusedLocationProviderResult; 4 | 5 | interface IFusedLocationProviderCallback { 6 | oneway void onFusedLocationProviderResult(in FusedLocationProviderResult result) = 0; 7 | } 8 | -------------------------------------------------------------------------------- /config-holder/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | dependencyResolutionManagement { 8 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 9 | repositories { 10 | google() 11 | mavenCentral() 12 | } 13 | } 14 | 15 | include(":app") 16 | -------------------------------------------------------------------------------- /lib/Android.bp: -------------------------------------------------------------------------------- 1 | android_app { 2 | name: "GmsCompatLib", 3 | platform_apis: true, 4 | certificate: "gmscompat_lib", 5 | srcs: [ 6 | "src/**/*.java", 7 | "src/**/*.aidl", 8 | ], 9 | resource_dirs: [ 10 | "res", 11 | ], 12 | manifest: "AndroidManifest.xml", 13 | 14 | optimize: { 15 | enabled: false, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/src/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.play.core.integrity.protocol; 2 | 3 | import android.os.Bundle; 4 | import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; 5 | 6 | interface IIntegrityService { 7 | oneway void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1; 8 | } 9 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/ILocationAvailabilityStatusCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import com.google.android.gms.common.api.Status; 4 | import com.google.android.gms.location.LocationAvailability; 5 | 6 | interface ILocationAvailabilityStatusCallback { 7 | void onResult(in Status status, in LocationAvailability locationAvailability); 8 | } 9 | -------------------------------------------------------------------------------- /config-holder/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /lib/src/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.play.core.integrity.protocol; 2 | 3 | import android.os.Bundle; 4 | import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; 5 | 6 | interface IExpressIntegrityService { 7 | oneway void requestIntegrityToken(in Bundle request, IExpressIntegrityServiceCallback callback) = 2; 8 | } 9 | -------------------------------------------------------------------------------- /src/com/google/android/gms/RoSafeParcelable.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms; 2 | 3 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 4 | import android.os.Parcel; 5 | 6 | public abstract class RoSafeParcelable extends AbstractSafeParcelable { 7 | @Override 8 | public final void writeToParcel(Parcel dest, int flags) { 9 | throw new IllegalStateException("this is a read-only SafeParcelable"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/ILocationCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.location.Location; 4 | 5 | import com.google.android.gms.location.LocationAvailability; 6 | import com.google.android.gms.location.LocationResult; 7 | 8 | interface ILocationCallback { 9 | void onLocationResult(in LocationResult result) = 0; 10 | void onLocationAvailability(in LocationAvailability availability) = 1; 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build application 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v5 11 | - name: Set up JDK 21 12 | uses: actions/setup-java@v5 13 | with: 14 | distribution: 'temurin' 15 | java-version: 21 16 | cache: gradle 17 | - name: Build with Gradle 18 | run: | 19 | cd config-holder 20 | ./gradlew build --no-daemon 21 | -------------------------------------------------------------------------------- /res/drawable/ic_configuration_required.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/util/BinderUtils.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.util; 2 | 3 | import android.annotation.Nullable; 4 | import android.os.IBinder; 5 | import android.os.RemoteException; 6 | import android.util.Log; 7 | 8 | public class BinderUtils { 9 | private static final String TAG = "BinderUtils"; 10 | 11 | @Nullable 12 | public static String getInterfaceDescriptor(IBinder binder) { 13 | try { 14 | return binder.getInterfaceDescriptor(); 15 | } catch (RemoteException e) { 16 | Log.d(TAG, "", e); 17 | return null; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/sysservice/client/ClientServiceOverridesRegistry.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.sysservice.client; 2 | 3 | import android.app.IActivityManager; 4 | import android.content.pm.IPackageManager; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | import android.util.ArrayMap; 8 | 9 | import java.util.function.Function; 10 | 11 | public class ClientServiceOverridesRegistry { 12 | 13 | public static void init(ArrayMap> registry) { 14 | registry.put(IActivityManager.Stub.DESCRIPTOR, GclActivityManager::new); 15 | registry.put(IPackageManager.Stub.DESCRIPTOR, GclPackageManager::new); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /res/drawable/ic_crash_report.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationAvailabilityRequest.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.SafeParcelable; 4 | import android.os.Parcelable; 5 | 6 | import com.google.android.gms.RoSafeParcelable; 7 | 8 | @SafeParcelable.Class(creator = "LocationAvailabilityRequestCreator") 9 | public class LocationAvailabilityRequest extends RoSafeParcelable { 10 | // @Field(id = 1) public boolean bypass; 11 | // @Field(id = 2) public ClientIdentity impersonation; 12 | 13 | @Constructor 14 | public LocationAvailabilityRequest() { 15 | } 16 | 17 | public static final Parcelable.Creator CREATOR = new LocationAvailabilityRequestCreator(); 18 | } 19 | -------------------------------------------------------------------------------- /lib/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit -o pipefail 4 | 5 | [[ -n $KEY_DIR ]] || { 6 | echo expected KEY_DIR in the environment 7 | exit 1 8 | } 9 | 10 | [[ -n $OS_BUILD_NUMBER ]] || { 11 | echo expected OS_BUILD_NUMBER in the environment 12 | exit 1 13 | } 14 | 15 | export OUT_DIR=out_gmscompat_lib 16 | 17 | source build/envsetup.sh 18 | lunch sdk_phone64_x86_64-cur-user 19 | m GmsCompatLib apksigner 20 | 21 | OUT=GmsCompatLibSigned.apk 22 | 23 | "$ANDROID_HOST_OUT"/bin/apksigner sign --min-sdk-version 36 \ 24 | --in "$ANDROID_PRODUCT_OUT"/system/app/GmsCompatLib/GmsCompatLib.apk --out $OUT \ 25 | --key "$KEY_DIR"/gmscompat_lib.pk8 --cert "$KEY_DIR"/gmscompat_lib.x509.pem 26 | 27 | PROPS="$OUT.props.toml" 28 | echo "requiredSystemFeatures = [\"grapheneos.version >= $OS_BUILD_NUMBER\"]" > $PROPS 29 | 30 | echo "Written $OUT, $OUT.idsig and $PROPS" 31 | -------------------------------------------------------------------------------- /res/drawable/ic_pending_action.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.app.compat.gms.GmsCompat 4 | import android.content.Intent 5 | import android.ext.PackageId 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity 10 | 11 | const val USAGE_GUIDE_URL = "https://grapheneos.org/usage#sandboxed-google-play" 12 | 13 | class MainActivity : CollapsingToolbarBaseActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.main_activity) 17 | 18 | if (!GmsCompat.isEnabledFor(PackageId.GMS_CORE_NAME, userId)) { 19 | val uri = Uri.parse(USAGE_GUIDE_URL) 20 | startActivity(Intent(Intent.ACTION_VIEW, uri)) 21 | finishAndRemoveTask() 22 | return 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/BinderDefSupplier.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.content.Context 4 | import android.os.Binder 5 | import android.os.BinderDef 6 | import java.util.Arrays 7 | import kotlin.reflect.KClass 8 | 9 | abstract class BinderDefSupplier(val ifaceName: String, implClass: KClass) { 10 | val className = implClass.java.name 11 | 12 | // Path of APK from which to load class that is referred to by className 13 | open fun apkPath(ctx: Context): String { 14 | return ctx.applicationInfo.sourceDir 15 | } 16 | 17 | // Binder transaction codes 18 | abstract fun transactionCodes(callerPkg: String): IntArray? 19 | 20 | fun get(ctx: Context, callerPkg: String): BinderDef { 21 | val txnCodes = transactionCodes(callerPkg) 22 | if (txnCodes != null) { 23 | Arrays.sort(txnCodes) 24 | } 25 | return BinderDef(ifaceName, apkPath(ctx), className, txnCodes) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /res/drawable/ic_screen_share.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/api/CommonStatusCodes.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common.api; 2 | 3 | // https://developers.google.com/android/reference/com/google/android/gms/common/api/CommonStatusCodes 4 | public class CommonStatusCodes { 5 | public static final int SUCCESS_CACHE = -1; 6 | public static final int SUCCESS = 0; 7 | public static final int SIGN_IN_REQUIRED = 4; 8 | public static final int INVALID_ACCOUNT = 5; 9 | public static final int RESOLUTION_REQUIRED = 6; 10 | public static final int NETWORK_ERROR = 7; 11 | public static final int INTERNAL_ERROR = 8; 12 | public static final int SERVICE_INVALID = 9; 13 | public static final int DEVELOPER_ERROR = 10; 14 | public static final int LICENSE_CHECK_FAILED = 11; 15 | public static final int ERROR = 13; 16 | public static final int INTERRUPTED = 14; 17 | public static final int TIMEOUT = 15; 18 | public static final int CANCELED = 16; 19 | public static final int API_NOT_CONNECTED = 17; 20 | } 21 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationResult.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 4 | import android.app.appsearch.safeparcel.SafeParcelable; 5 | import android.location.Location; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | 9 | import java.util.List; 10 | 11 | // https://developers.google.com/android/reference/com/google/android/gms/location/LocationResult 12 | @SafeParcelable.Class(creator = "LocationResultCreator") 13 | public class LocationResult extends AbstractSafeParcelable { 14 | @Field(id = 1) public final List locations; 15 | 16 | @Constructor 17 | public LocationResult(@Param(id = 1) List locations) { 18 | this.locations = locations; 19 | } 20 | 21 | @Override 22 | public void writeToParcel(Parcel dest, int flags) { 23 | LocationResultCreator.writeToParcel(this, dest, flags); 24 | } 25 | 26 | public static final Parcelable.Creator CREATOR = null; 27 | } 28 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/FusedLocationProviderResult.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 4 | import android.app.appsearch.safeparcel.SafeParcelable; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | import com.google.android.gms.common.api.Status; 9 | 10 | @SafeParcelable.Class(creator = "FusedLocationProviderResultCreator") 11 | public class FusedLocationProviderResult extends AbstractSafeParcelable { 12 | public static final FusedLocationProviderResult SUCCESS = new FusedLocationProviderResult(Status.SUCCESS); 13 | 14 | @Field(id = 1) public Status status; 15 | 16 | @Constructor 17 | public FusedLocationProviderResult(@Param(id = 1) Status status) { 18 | this.status = status; 19 | } 20 | 21 | @Override 22 | public void writeToParcel(Parcel dest, int flags) { 23 | FusedLocationProviderResultCreator.writeToParcel(this, dest, flags); 24 | } 25 | 26 | public static final Parcelable.Creator CREATOR = null; 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2022-2025 GrapheneOS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LastLocationRequest.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.SafeParcelable; 4 | import android.os.Parcelable; 5 | 6 | import com.google.android.gms.RoSafeParcelable; 7 | 8 | // https://developers.google.com/android/reference/com/google/android/gms/location/LastLocationRequest 9 | @SafeParcelable.Class(creator = "LastLocationRequestCreator") 10 | public class LastLocationRequest extends RoSafeParcelable { 11 | @Field(id = 1, defaultValueUnchecked = "Long.MAX_VALUE") public long maxAge; 12 | @Field(id = 2, defaultValueUnchecked = "LocationRequest.GRANULARITY_PERMISSION_LEVEL") public int granularity; 13 | // @Field(id = 3) public boolean locationSettingsIgnored; // also named "bypass" 14 | // @Field(id = 4) public String moduleId; 15 | // @Field(id = 5) public ClientIdentity impersonation; 16 | 17 | @Constructor 18 | public LastLocationRequest(@Param(id = 1) long maxAge, @Param(id = 2) int granularity) { 19 | this.maxAge = maxAge; 20 | this.granularity = granularity; 21 | } 22 | 23 | public static final Parcelable.Creator CREATOR = new LastLocationRequestCreator(); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/AbsContentProvider.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | 11 | public abstract class AbsContentProvider extends ContentProvider { 12 | 13 | public boolean onCreate() { 14 | App.maybeInit(getContext()); 15 | return true; 16 | } 17 | 18 | @Nullable 19 | public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { 20 | return null; 21 | } 22 | 23 | @Nullable 24 | public String getType(@NonNull Uri uri) { 25 | return null; 26 | } 27 | 28 | @Nullable 29 | public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 30 | return null; 31 | } 32 | 33 | public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { 34 | return 0; 35 | } 36 | 37 | public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { 38 | return 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationSettingsRequest.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.SafeParcelable; 4 | import android.os.Parcelable; 5 | 6 | import java.util.List; 7 | 8 | import app.grapheneos.gmscompat.UtilsKt; 9 | import com.google.android.gms.RoSafeParcelable; 10 | 11 | // https://developers.google.com/android/reference/com/google/android/gms/location/LocationSettingsRequest.Builder 12 | @SafeParcelable.Class(creator = "LocationSettingsRequestCreator") 13 | public class LocationSettingsRequest extends RoSafeParcelable { 14 | @Field(id = 1) public List requests; 15 | @Field(id = 2) public boolean alwaysShow; 16 | @Field(id = 3) public boolean needBle; 17 | 18 | @Constructor 19 | public LocationSettingsRequest(@Param(id = 1) List requests, 20 | @Param(id = 2) boolean alwaysShow, @Param(id = 3) boolean needBle) { 21 | this.requests = requests; 22 | this.alwaysShow = alwaysShow; 23 | this.needBle = needBle; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return UtilsKt.objectToString(this); 29 | } 30 | 31 | public static final Parcelable.Creator CREATOR = new LocationSettingsRequestCreator(); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.util; 2 | 3 | import android.content.ComponentName; 4 | import android.content.ServiceConnection; 5 | import android.os.IBinder; 6 | 7 | import java.util.function.UnaryOperator; 8 | 9 | public class ServiceConnectionWrapper implements ServiceConnection { 10 | private final ServiceConnection base; 11 | private final UnaryOperator binderOverride; 12 | 13 | public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator binderOverride) { 14 | this.base = base; 15 | this.binderOverride = binderOverride; 16 | } 17 | 18 | @Override 19 | public void onServiceConnected(ComponentName name, IBinder service) { 20 | IBinder override = binderOverride.apply(service); 21 | if (override != null) { 22 | service = override; 23 | } 24 | base.onServiceConnected(name, service); 25 | } 26 | 27 | @Override 28 | public void onServiceDisconnected(ComponentName name) { 29 | base.onServiceDisconnected(name); 30 | } 31 | 32 | @Override 33 | public void onBindingDied(ComponentName name) { 34 | base.onBindingDied(name); 35 | } 36 | 37 | @Override 38 | public void onNullBinding(ComponentName name) { 39 | base.onNullBinding(name); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationSettingsResult.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 4 | import android.app.appsearch.safeparcel.SafeParcelable; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | import com.google.android.gms.common.api.Result; 9 | import com.google.android.gms.common.api.Status; 10 | 11 | // https://developers.google.com/android/reference/com/google/android/gms/location/LocationSettingsResult 12 | @SafeParcelable.Class(creator = "LocationSettingsResultCreator") 13 | public class LocationSettingsResult extends AbstractSafeParcelable implements Result { 14 | @Field(id = 1) public Status status; 15 | @Field(id = 2) public LocationSettingsStates settings; 16 | 17 | @Constructor 18 | public LocationSettingsResult(@Param(id = 1) Status status, 19 | @Param(id = 2) LocationSettingsStates settings) { 20 | this.status = status; 21 | this.settings = settings; 22 | } 23 | 24 | @Override 25 | public Status getStatus() { 26 | return status; 27 | } 28 | 29 | @Override 30 | public void writeToParcel(Parcel dest, int flags) { 31 | LocationSettingsResultCreator.writeToParcel(this, dest, flags); 32 | } 33 | 34 | public static final Parcelable.Creator CREATOR = null; 35 | } 36 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/LocationReceiver.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import android.app.PendingIntent; 4 | import android.app.appsearch.safeparcel.SafeParcelable; 5 | import android.os.IBinder; 6 | import android.os.Parcelable; 7 | 8 | import com.google.android.gms.RoSafeParcelable; 9 | 10 | @SafeParcelable.Class(creator = "LocationReceiverCreator") 11 | public class LocationReceiver extends RoSafeParcelable { 12 | public static final int TYPE_ILocationListener = 1; 13 | public static final int TYPE_ILocationCallback = 2; 14 | public static final int TYPE_PendingIntent = 3; 15 | public static final int TYPE_ILocationStatusCallback = 4; 16 | public static final int TYPE_ILocationAvailabilityStatusCallback = 5; 17 | 18 | @Field(id = 1) public int type; 19 | // @Field(id = 2) public IBinder; 20 | @Field(id = 3) public IBinder binder; 21 | @Field(id = 4) public PendingIntent pendingIntent; 22 | // @Field(id = 5) public String; 23 | // @Field(id = 6) public String; 24 | 25 | @Constructor 26 | public LocationReceiver(@Param(id = 1) int type, @Param(id = 3) IBinder binder, @Param(id = 4) PendingIntent pendingIntent) { 27 | this.type = type; 28 | this.binder = binder; 29 | this.pendingIntent = pendingIntent; 30 | } 31 | 32 | public static final Parcelable.Creator CREATOR = new LocationReceiverCreator(); 33 | } 34 | -------------------------------------------------------------------------------- /Android.bp: -------------------------------------------------------------------------------- 1 | android_app { 2 | name: "GmsCompat", 3 | platform_apis: true, 4 | srcs: [ 5 | "src/**/*.java", 6 | "src/**/*.kt", 7 | "src/**/*.aidl", 8 | ":framework-appsearch-safeparcel-sources", 9 | ], 10 | aidl: { 11 | local_include_dirs: ["src"], 12 | }, 13 | plugins: [ 14 | "safeparcel-annotation-processor", 15 | ], 16 | resource_dirs: [ 17 | "res", 18 | ], 19 | static_libs: [ 20 | "androidx.preference_preference", 21 | // needed by framework-appsearch-safeparcel-sources 22 | "appsearch_flags_java_exported_lib", 23 | "SettingsLibCategory", 24 | "SettingsLibCollapsingToolbarBaseActivity", 25 | ], 26 | jarjar_rules: "jarjar-rules.txt", 27 | 28 | manifest: "AndroidManifest.xml", 29 | 30 | required: [ 31 | "GmsCompatConfig", 32 | "GmsCompatLib", 33 | "etc_default-permissions_app.grapheneos.gmscompat.xml", 34 | "etc_sysconfig_app.grapheneos.gmscompat.xml", 35 | ], 36 | } 37 | 38 | prebuilt_etc { 39 | name: "etc_default-permissions_app.grapheneos.gmscompat.xml", 40 | src: "etc/default-permissions/app.grapheneos.gmscompat.xml", 41 | sub_dir: "default-permissions", 42 | filename_from_src: true, 43 | } 44 | 45 | prebuilt_etc { 46 | name: "etc_sysconfig_app.grapheneos.gmscompat.xml", 47 | src: "etc/sysconfig/app.grapheneos.gmscompat.xml", 48 | sub_dir: "sysconfig", 49 | filename_from_src: true, 50 | } 51 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/ConfigUpdateReceiver.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.util.Log; 8 | 9 | import static android.os.PatternMatcher.PATTERN_LITERAL; 10 | import static app.grapheneos.gmscompat.Const.IS_DEV_BUILD; 11 | 12 | public class ConfigUpdateReceiver extends BroadcastReceiver { 13 | public static final String CONFIG_HOLDER_PACKAGE = "app.grapheneos.gmscompat.config"; 14 | public static final String CONFIG_HOLDER_PACKAGE_DEV = CONFIG_HOLDER_PACKAGE + ".dev"; 15 | 16 | public ConfigUpdateReceiver(Context ctx) { 17 | IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REPLACED); 18 | filter.addDataScheme("package"); 19 | filter.addDataSchemeSpecificPart(CONFIG_HOLDER_PACKAGE, PATTERN_LITERAL); 20 | 21 | if (IS_DEV_BUILD) { 22 | filter.addAction(Intent.ACTION_PACKAGE_ADDED); 23 | filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 24 | filter.addDataSchemeSpecificPart(CONFIG_HOLDER_PACKAGE_DEV, PATTERN_LITERAL); 25 | } 26 | 27 | ctx.registerReceiver(this, filter); 28 | } 29 | 30 | @Override 31 | public void onReceive(Context context, Intent intent) { 32 | Log.d("ConfigUpdateReceiver", "" + intent + " | uri: " + intent.getData()); 33 | BinderGms2Gca.INSTANCE.parseAndUpdateConfig(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/location/StubResolutionActivity.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.location; 2 | 3 | import android.annotation.Nullable; 4 | import android.app.Activity; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.widget.Toast; 8 | 9 | import app.grapheneos.gmscompat.R; 10 | 11 | public class StubResolutionActivity extends Activity { 12 | private static final String TAG = "StubResolutionActivity"; 13 | 14 | public static final String ID_LOCATION_ACCESS_IS_GLOBALLY_DISABLED = "location_is_globally_disabled"; 15 | 16 | @Override 17 | protected void onCreate(@Nullable Bundle savedInstanceState) { 18 | Log.d(TAG, "onCreate, callingActivity: " + getCallingActivity()); 19 | super.onCreate(savedInstanceState); 20 | setResult(RESULT_CANCELED); 21 | String id = getIntent().getIdentifier(); 22 | if (id != null) { 23 | switch (id) { 24 | case ID_LOCATION_ACCESS_IS_GLOBALLY_DISABLED -> 25 | showToast(getText(R.string.toast_app_location_access_is_globally_off)); 26 | } 27 | } 28 | finish(); 29 | } 30 | 31 | private static Toast prevToast; 32 | 33 | private void showToast(CharSequence text) { 34 | Toast prev = prevToast; 35 | if (prev != null) { 36 | prev.cancel(); 37 | prevToast = null; 38 | } 39 | Toast t = Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT); 40 | t.show(); 41 | prevToast = t; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/RpcProvider.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.app.compat.gms.GmsCompat 4 | import android.content.pm.PackageManager 5 | import android.os.Binder 6 | import android.os.Bundle 7 | 8 | import com.android.internal.gmscompat.GmsCompatApp 9 | 10 | class RpcProvider : AbsContentProvider() { 11 | override fun call(whichStr: String, arg: String?, extras: Bundle?): Bundle? { 12 | val ctx = requireContext() 13 | return when (Integer.parseInt(whichStr)) { 14 | GmsCompatApp.RPC_GET_BINDER_IGms2Gca -> { 15 | // WRITE_GSERVICES is a signature-protected permission held by GmsCore and Play Store 16 | if (ctx.checkCallingPermission("com.google.android.providers.gsf.permission.WRITE_GSERVICES") != PackageManager.PERMISSION_GRANTED) { 17 | if (!GmsCompat.isEnabledFor(getCallingPackage()!!, ctx.userId)) { 18 | throw SecurityException() 19 | } 20 | } 21 | 22 | wrapBinder(BinderGms2Gca) 23 | } 24 | GmsCompatApp.RPC_GET_BINDER_IClientOfGmsCore2Gca -> { 25 | // any client of GMS Core is allowed to access this binder 26 | wrapBinder(BinderClientOfGmsCore2Gca) 27 | } 28 | else -> throw IllegalArgumentException(whichStr) 29 | } 30 | } 31 | 32 | private fun wrapBinder(binder: Binder): Bundle { 33 | val bundle = Bundle(1) 34 | bundle.putBinder(GmsCompatApp.KEY_BINDER, binder) 35 | return bundle 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/ConnectionResult.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common; 2 | 3 | import android.app.PendingIntent; 4 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 5 | import android.app.appsearch.safeparcel.SafeParcelable; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | 9 | import app.grapheneos.gmscompat.UtilsKt; 10 | 11 | // https://developers.google.com/android/reference/com/google/android/gms/common/ConnectionResult 12 | @SafeParcelable.Class(creator = "ConnectionResultCreator") 13 | public class ConnectionResult extends AbstractSafeParcelable { 14 | @Field(id = 1) public int unknown1; 15 | @Field(id = 2) public int statusCode; 16 | @Field(id = 3) public PendingIntent resolution; 17 | @Field(id = 4) public String message; 18 | 19 | public static final ConnectionResult SUCCESS = new ConnectionResult(0, 0, null, null); 20 | 21 | @Constructor 22 | public ConnectionResult(@Param(id = 1) int unknown1, @Param(id = 2) int statusCode, 23 | @Param(id = 3) PendingIntent resolution, @Param(id = 4) String message) { 24 | this.unknown1 = unknown1; 25 | this.statusCode = statusCode; 26 | this.resolution = resolution; 27 | this.message = message; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return UtilsKt.objectToString(this); 33 | } 34 | 35 | @Override 36 | public void writeToParcel(Parcel dest, int flags) { 37 | ConnectionResultCreator.writeToParcel(this, dest, flags); 38 | } 39 | 40 | public static final Parcelable.Creator CREATOR = null; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/location/Client.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.location 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.ContextParams 6 | import android.content.pm.PackageManager 7 | import android.location.LocationManager 8 | 9 | class Client(val gls: GLocationService, pkgName: String? = null, val attributionTag: String? = null) { 10 | val packageName: String 11 | val permission: Permission 12 | val ctx: Context 13 | 14 | init { 15 | val glsCtx = gls.ctx 16 | 17 | packageName = pkgName ?: glsCtx.packageName 18 | 19 | permission = if (Permission.FINE.isGranted(glsCtx)) { 20 | Permission.FINE 21 | } else { 22 | if (!Permission.COARSE.isGranted(glsCtx)) { 23 | throw SecurityException("no location permission") 24 | } 25 | Permission.COARSE 26 | } 27 | 28 | ctx = if (attributionTag != null) { 29 | val cp = ContextParams.Builder().run { 30 | setAttributionTag(attributionTag) 31 | build() 32 | } 33 | glsCtx.createContext(cp) 34 | } else { 35 | glsCtx 36 | } 37 | } 38 | 39 | val locationManager = ctx.getSystemService(LocationManager::class.java)!! 40 | } 41 | 42 | enum class Permission(val string: String) { 43 | COARSE(Manifest.permission.ACCESS_COARSE_LOCATION), 44 | FINE(Manifest.permission.ACCESS_FINE_LOCATION), 45 | ; 46 | 47 | fun isGranted(ctx: Context): Boolean { 48 | return ctx.checkSelfPermission(string) == PackageManager.PERMISSION_GRANTED 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/LocationRequestInternal.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import android.app.appsearch.safeparcel.SafeParcelable; 4 | import android.os.Parcelable; 5 | 6 | import com.google.android.gms.location.LocationRequest; 7 | 8 | import com.google.android.gms.RoSafeParcelable; 9 | 10 | @SafeParcelable.Class(creator = "LocationRequestInternalCreator") 11 | public class LocationRequestInternal extends RoSafeParcelable { 12 | @Field(id = 1) public LocationRequest request; 13 | 14 | /* 15 | these fields are never set by the recent GMS client versions 16 | 17 | @Field(id = 5) public List clients; 18 | @Field(id = 6) public String tag; 19 | @Field(id = 7) public boolean hideAppOps; 20 | @Field(id = 8) public boolean forceCoarseLocation; 21 | @Field(id = 9) public boolean exemptFromBackgroundThrottle; 22 | @Field(id = 10) public String moduleId; 23 | @Field(id = 11) public boolean locationSettingsIgnored; 24 | */ 25 | 26 | @Field(id = 13) public String contextAttributionTag; 27 | /* 28 | these fields aren't necessary 29 | 30 | @Field(id = 12) public boolean inaccurateLocationsDelayed; 31 | @Field(id = 14) public long maxLocationAgeMillis; 32 | */ 33 | 34 | @Constructor 35 | public LocationRequestInternal(@Param(id = 1) LocationRequest request, @Param(id = 13) String contextAttributionTag) { 36 | this.request = request; 37 | this.contextAttributionTag = contextAttributionTag; 38 | } 39 | 40 | public static final Parcelable.Creator CREATOR = new LocationRequestInternalCreator(); 41 | } 42 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/CurrentLocationRequest.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.SafeParcelable; 4 | import android.os.Parcelable; 5 | 6 | import com.google.android.gms.RoSafeParcelable; 7 | 8 | // https://developers.google.com/android/reference/com/google/android/gms/location/CurrentLocationRequest 9 | @SafeParcelable.Class(creator = "CurrentLocationRequestCreator") 10 | public class CurrentLocationRequest extends RoSafeParcelable { 11 | @Field(id = 1, defaultValue = "60000") public long maxUpdateAgeMillis; 12 | @Field(id = 2, defaultValueUnchecked = "LocationRequest.GRANULARITY_PERMISSION_LEVEL") public int granularity; 13 | @Field(id = 3, defaultValueUnchecked = "LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY") public int priority; 14 | @Field(id = 4, defaultValueUnchecked = "Long.MAX_VALUE") public long durationMillis; 15 | // @Field(id = 5) public boolean bypass; 16 | // @Field(id = 6) public WorkSource workSource; 17 | // @Field(id = 7) public int throttleBehavior; 18 | // @Field(id = 8) public String moduleId; 19 | // @Field(id = 9) public ClientIdentity impersonation; 20 | 21 | @Constructor 22 | public CurrentLocationRequest(@Param(id = 1) long maxUpdateAgeMillis, @Param(id = 2) int granularity, 23 | @Param(id = 3) int priority, @Param(id = 4) long durationMillis) { 24 | this.maxUpdateAgeMillis = maxUpdateAgeMillis; 25 | this.granularity = granularity; 26 | this.priority = priority; 27 | this.durationMillis = durationMillis; 28 | } 29 | 30 | public static final Parcelable.Creator CREATOR = new CurrentLocationRequestCreator(); 31 | } 32 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationSettingsStates.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 4 | import android.app.appsearch.safeparcel.SafeParcelable; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | // https://developers.google.com/android/reference/com/google/android/gms/location/LocationSettingsStates 9 | @SafeParcelable.Class(creator = "LocationSettingsStatesCreator") 10 | public class LocationSettingsStates extends AbstractSafeParcelable { 11 | @Field(id = 1) public boolean gpsUsable; 12 | @Field(id = 2) public boolean networkLocationUsable; 13 | @Field(id = 3) public boolean bleUsable; 14 | @Field(id = 4) public boolean gpsPresent; 15 | @Field(id = 5) public boolean networkLocationPresent; 16 | @Field(id = 6) public boolean blePresent; 17 | 18 | public LocationSettingsStates() {} 19 | 20 | @Constructor 21 | public LocationSettingsStates(@Param(id = 1) boolean gpsUsable, 22 | @Param(id = 2) boolean networkLocationUsable, 23 | @Param(id = 3) boolean bleUsable, @Param(id = 4) boolean gpsPresent, 24 | @Param(id = 5) boolean networkLocationPresent, @Param(id = 6) boolean blePresent) { 25 | this.gpsUsable = gpsUsable; 26 | this.networkLocationUsable = networkLocationUsable; 27 | this.bleUsable = bleUsable; 28 | this.gpsPresent = gpsPresent; 29 | this.networkLocationPresent = networkLocationPresent; 30 | this.blePresent = blePresent; 31 | } 32 | 33 | @Override 34 | public void writeToParcel(Parcel dest, int flags) { 35 | LocationSettingsStatesCreator.writeToParcel(this, dest, flags); 36 | } 37 | 38 | public static final Parcelable.Creator CREATOR = null; 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.playintegrity; 2 | 3 | import android.os.Binder; 4 | import android.os.Bundle; 5 | import android.os.IBinder; 6 | import android.os.RemoteException; 7 | import android.util.Log; 8 | 9 | import com.android.internal.os.BackgroundThread; 10 | import com.google.android.play.core.integrity.protocol.IIntegrityService; 11 | import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; 12 | 13 | class ClassicPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { 14 | 15 | ClassicPlayIntegrityServiceWrapper(IBinder base) { 16 | super(base); 17 | requestIntegrityTokenTxnCode = 2; // IIntegrityService.Stub.TRANSACTION_requestIntegrityToken 18 | } 19 | 20 | static class TokenRequestStub extends IIntegrityService.Stub { 21 | public void requestIntegrityToken(Bundle request, IIntegrityServiceCallback callback) { 22 | Runnable r = () -> { 23 | var result = new Bundle(); 24 | // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/IntegrityErrorCode.html#API_NOT_AVAILABLE 25 | final int API_NOT_AVAILABLE = -1; 26 | result.putInt("error", API_NOT_AVAILABLE); 27 | try { 28 | callback.onResult(result); 29 | } catch (RemoteException e) { 30 | Log.e("IIntegrityService.Stub", "", e); 31 | } 32 | }; 33 | BackgroundThread.getHandler().postDelayed(r, getTokenRequestResultDelay()); 34 | } 35 | }; 36 | 37 | @Override 38 | protected Binder createTokenRequestStub() { 39 | return new TokenRequestStub(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/sysservice/SystemServiceOverridesRegistry.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.sysservice; 2 | 3 | import android.app.ActivityManager; 4 | import android.app.ActivityThread; 5 | import android.app.AppGlobals; 6 | import android.app.IActivityManager; 7 | import android.app.SystemServiceRegistry; 8 | import android.app.compat.gms.GmsCompat; 9 | import android.content.Context; 10 | import android.ext.PackageId; 11 | import android.hardware.display.DisplayManagerGlobal; 12 | import android.os.IBinder; 13 | import android.os.IInterface; 14 | import android.util.ArrayMap; 15 | 16 | import java.util.function.Function; 17 | 18 | import app.grapheneos.gmscompat.lib.GmsCompatLibImpl; 19 | import app.grapheneos.gmscompat.lib.sysservice.client.ClientServiceOverridesRegistry; 20 | 21 | public class SystemServiceOverridesRegistry { 22 | 23 | public static void init(Context appContext, ArrayMap> registry) { 24 | // Clear cached binder wrappers to support overriding them through 25 | /** @see GmsCompatLibImpl#maybeProvideBinderProxyInterface */ 26 | // Few wrappers are cached at this point and all or almost all of them are backed by cached 27 | // binders, i.e. their reinitialization will happen locally, without IPC 28 | ActivityManager.clearCachedService(); 29 | ActivityThread.clearCachedPackageManager(); 30 | DisplayManagerGlobal.clearCachedInstance(); 31 | SystemServiceRegistry.clearServiceCache(appContext); 32 | 33 | if (GmsCompat.isEnabled()) { 34 | registry.put(IActivityManager.Stub.DESCRIPTOR, GmcActivityManager::new); 35 | } else if (AppGlobals.getInitialPackageId() == PackageId.G_CAMERA) { 36 | ClientServiceOverridesRegistry.init(registry); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.playintegrity; 2 | 3 | import android.os.Binder; 4 | import android.os.Bundle; 5 | import android.os.IBinder; 6 | import android.os.RemoteException; 7 | import android.util.Log; 8 | 9 | import com.android.internal.os.BackgroundThread; 10 | import com.google.android.play.core.integrity.protocol.IExpressIntegrityService; 11 | import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; 12 | 13 | class StandardPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { 14 | 15 | StandardPlayIntegrityServiceWrapper(IBinder base) { 16 | super(base); 17 | requestIntegrityTokenTxnCode = 3; // IExpressIntegrityService.Stub.TRANSACTION_requestIntegrityToken 18 | } 19 | 20 | static class TokenRequestStub extends IExpressIntegrityService.Stub { 21 | public void requestIntegrityToken(Bundle request, IExpressIntegrityServiceCallback callback) { 22 | Runnable r = () -> { 23 | var result = new Bundle(); 24 | // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.html#API_NOT_AVAILABLE 25 | final int API_NOT_AVAILABLE = -1; 26 | result.putInt("error", API_NOT_AVAILABLE); 27 | try { 28 | callback.onRequestExpressIntegrityTokenResult(result); 29 | } catch (RemoteException e) { 30 | Log.e("IExpressIntegrityService.Stub", "", e); 31 | } 32 | }; 33 | BackgroundThread.getHandler().postDelayed(r, getTokenRequestResultDelay()); 34 | } 35 | }; 36 | 37 | @Override 38 | protected Binder createTokenRequestStub() { 39 | return new TokenRequestStub(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/util/Certs.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.util; 2 | 3 | import android.content.pm.Signature; 4 | import android.util.Base64; 5 | 6 | public class Certs { 7 | public static Signature gmsCore() { 8 | String base64 = "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK"; 9 | return makeCert(base64); 10 | } 11 | 12 | private static Signature makeCert(String base64) { 13 | byte[] cert = Base64.decode(base64, 0); 14 | return new Signature(cert); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationAvailability.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 4 | import android.app.appsearch.safeparcel.SafeParcelable; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | // https://developers.google.com/android/reference/com/google/android/gms/location/LocationAvailability 9 | @SafeParcelable.Class(creator = "LocationAvailabilityCreator") 10 | public class LocationAvailability extends AbstractSafeParcelable { 11 | /* 12 | @Deprecated 13 | @Field(id = 1) public int cellStatus; 14 | @Deprecated 15 | @Field(id = 2) public int wifiStatus; 16 | @Field(id = 3) public long elapsedRealtimeNs; 17 | */ 18 | @Field(id = 4) public int locationStatus; 19 | // @Deprecated @Field(id = 5) public NetworkLocationStatus[] 20 | @Field(id = 6) public boolean isAvailable; 21 | 22 | private static final int STATUS_AVAILABLE = 0; 23 | private static final int STATUS_UNAVAILABLE = 1000; 24 | 25 | @Constructor 26 | LocationAvailability(@Param(id = 4) int locationStatus, @Param(id = 6) boolean isAvailable) { 27 | this.locationStatus = locationStatus; 28 | this.isAvailable = isAvailable; 29 | } 30 | 31 | private static final LocationAvailability AVAILABLE = new LocationAvailability(STATUS_AVAILABLE, true); 32 | private static final LocationAvailability UNAVAILABLE = new LocationAvailability(STATUS_UNAVAILABLE, false); 33 | 34 | public static LocationAvailability get(boolean available) { 35 | return available? AVAILABLE : UNAVAILABLE; 36 | } 37 | 38 | public boolean isLocationAvailable() { 39 | return locationStatus < STATUS_UNAVAILABLE; 40 | } 41 | 42 | @Override 43 | public void writeToParcel(Parcel dest, int flags) { 44 | LocationAvailabilityCreator.writeToParcel(this, dest, flags); 45 | } 46 | 47 | public static final Parcelable.Creator CREATOR = null; 48 | } 49 | -------------------------------------------------------------------------------- /config-holder/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | import java.io.FileInputStream 3 | 4 | plugins { 5 | id("com.android.application") 6 | } 7 | 8 | java { 9 | toolchain { 10 | languageVersion.set(JavaLanguageVersion.of(17)) 11 | } 12 | } 13 | 14 | android { 15 | namespace = "app.grapheneos.gmscompat.config" 16 | 17 | compileSdk = 36 18 | buildToolsVersion = "36.1.0" 19 | 20 | defaultConfig { 21 | minSdk = 32 22 | targetSdk = 36 23 | versionCode = 167 24 | versionName = versionCode.toString() 25 | } 26 | 27 | sourceSets.getByName("main") { 28 | manifest.srcFile("AndroidManifest.xml") 29 | res.srcDir("res") 30 | resources.srcDir("../../gmscompat_config") 31 | } 32 | 33 | val keystorePropertiesFile = rootProject.file("keystore.properties") 34 | val useKeystoreProperties = keystorePropertiesFile.canRead() 35 | 36 | if (useKeystoreProperties) { 37 | val keystoreProperties = Properties() 38 | keystoreProperties.load(FileInputStream(keystorePropertiesFile)) 39 | 40 | signingConfigs { 41 | create("release") { 42 | storeFile = rootProject.file(keystoreProperties["storeFile"]!!) 43 | storePassword = keystoreProperties["storePassword"] as String 44 | keyAlias = keystoreProperties["keyAlias"] as String 45 | keyPassword = keystoreProperties["keyPassword"] as String 46 | enableV4Signing = true 47 | } 48 | } 49 | } 50 | 51 | buildTypes { 52 | getByName("release") { 53 | isMinifyEnabled = true 54 | if (useKeystoreProperties) { 55 | signingConfig = signingConfigs.getByName("release") 56 | } 57 | } 58 | 59 | create("dev") { 60 | initWith(getByName("release")) 61 | applicationIdSuffix = ".dev" 62 | signingConfig = signingConfigs.getByName("debug") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.playintegrity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.ServiceConnection; 6 | import android.content.pm.GosPackageState; 7 | import android.ext.PackageId; 8 | import android.ext.settings.app.AswBlockPlayIntegrityApi; 9 | import android.os.IBinder; 10 | 11 | import java.util.function.UnaryOperator; 12 | 13 | import app.grapheneos.gmscompat.lib.util.ServiceConnectionWrapper; 14 | 15 | import static android.app.compat.gms.GmsCompat.appContext; 16 | 17 | public class PlayIntegrityUtils { 18 | private static final String TAG = "PlayIntegrityUtils"; 19 | 20 | public static ServiceConnection maybeReplaceServiceConnection(Intent service, ServiceConnection orig) { 21 | if (PackageId.PLAY_STORE_NAME.equals(service.getPackage())) { 22 | UnaryOperator binderOverride = null; 23 | 24 | final String CLASSIC_SERVICE = 25 | "com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE"; 26 | final String STANDARD_SERVICE = 27 | "com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE"; 28 | 29 | String action = service.getAction(); 30 | if (STANDARD_SERVICE.equals(action)) { 31 | binderOverride = StandardPlayIntegrityServiceWrapper::new; 32 | } else if (CLASSIC_SERVICE.equals(action)) { 33 | binderOverride = ClassicPlayIntegrityServiceWrapper::new; 34 | } 35 | 36 | if (binderOverride != null) { 37 | return new ServiceConnectionWrapper(orig, binderOverride); 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | static boolean isPlayIntegrityBlocked() { 44 | Context ctx = appContext(); 45 | return AswBlockPlayIntegrityApi.I.get(ctx, ctx.getUserId(), ctx.getApplicationInfo(), GosPackageState.getForSelf(ctx)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/sysservice/GmcActivityManager.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.sysservice; 2 | 3 | import android.app.ForegroundServiceStartNotAllowedException; 4 | import android.app.IActivityManager; 5 | import android.app.IApplicationThread; 6 | import android.app.compat.gms.GmsCompat; 7 | import android.content.ComponentName; 8 | import android.content.Intent; 9 | import android.os.IBinder; 10 | import android.os.RemoteException; 11 | import android.util.Log; 12 | 13 | import com.android.internal.gmscompat.GmsCompatApp; 14 | 15 | class GmcActivityManager extends IActivityManager.Stub.Proxy { 16 | static final String TAG = "GmcActivityManager"; 17 | 18 | protected GmcActivityManager(IBinder remote) { 19 | super(remote); 20 | } 21 | 22 | @Override 23 | public ComponentName startService(IApplicationThread caller, Intent service, String resolvedType, boolean requireForeground, String callingPackage, String callingFeatureId, int userId) throws RemoteException { 24 | try { 25 | ComponentName res = super.startService(caller, service, resolvedType, requireForeground, callingPackage, callingFeatureId, userId); 26 | if (res == null || !res.getPackageName().equals("?")) { 27 | return res; 28 | } 29 | } catch (ForegroundServiceStartNotAllowedException e) { 30 | Log.d(TAG, "", e); 31 | } 32 | raiseSelfToForeground(service, requireForeground); 33 | return super.startService(caller, service, resolvedType, requireForeground, callingPackage, callingFeatureId, userId); 34 | } 35 | 36 | private static void raiseSelfToForeground(Intent service, boolean requireForeground) { 37 | Log.d(TAG, "unable to start " + service + ", requireForeground: " + requireForeground); 38 | String reason = "GmsCompat: " + service + ", requireForeground: " + requireForeground; 39 | GmsCompatApp.raisePackageToForeground(GmsCompat.appContext().getPackageName(), 40 | 30_000, reason, android.os.PowerExemptionManager.REASON_OTHER); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/google/android/gms/common/api/Status.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.common.api; 2 | 3 | import android.app.PendingIntent; 4 | import android.app.appsearch.safeparcel.AbstractSafeParcelable; 5 | import android.app.appsearch.safeparcel.SafeParcelable; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | 9 | import com.google.android.gms.common.ConnectionResult; 10 | 11 | import app.grapheneos.gmscompat.UtilsKt; 12 | 13 | // https://developers.google.com/android/reference/com/google/android/gms/common/api/Status 14 | @SafeParcelable.Class(creator = "StatusCreator") 15 | public final class Status extends AbstractSafeParcelable implements Result { 16 | public static final Status SUCCESS = new Status(CommonStatusCodes.SUCCESS, null, null, ConnectionResult.SUCCESS); 17 | 18 | @Field(id = 1) public int statusCode; 19 | @Field(id = 2) public String statusMessage; 20 | @Field(id = 3) public PendingIntent resolution; 21 | @Field(id = 4) public ConnectionResult connectionResult; 22 | 23 | @Constructor 24 | public Status(@Param(id = 1) int statusCode, @Param(id = 2) String statusMessage, 25 | @Param(id = 3) PendingIntent resolution, @Param(id = 4) ConnectionResult connectionResult) { 26 | this.statusCode = statusCode; 27 | this.statusMessage = statusMessage; 28 | this.resolution = resolution; 29 | this.connectionResult = connectionResult; 30 | } 31 | 32 | public Status(int statusCode) { 33 | this(statusCode, null, null, null); 34 | } 35 | 36 | @Override 37 | public Status getStatus() { 38 | return this; 39 | } 40 | 41 | public boolean isSuccess() { 42 | // there is SUCCESS (0) and SUCCESS_CACHE (-1) 43 | return statusCode <= CommonStatusCodes.SUCCESS; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return UtilsKt.objectToString(this); 49 | } 50 | 51 | @Override 52 | public void writeToParcel(Parcel dest, int flags) { 53 | StatusCreator.writeToParcel(this, dest, flags); 54 | } 55 | 56 | public static final Parcelable.Creator CREATOR = null; 57 | } 58 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/location/Listeners.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.location 2 | 3 | import android.util.ArrayMap 4 | import android.util.Log 5 | import app.grapheneos.gmscompat.logd 6 | 7 | // all registered listeners. key is either Binder or PendingIntent 8 | typealias Listeners = ArrayMap 9 | 10 | private val MAX_COUNT = 100 11 | 12 | fun getAllOsLocationListeners(listeners: Listeners): List { 13 | synchronized(listeners) { 14 | val size = listeners.size 15 | val res = ArrayList(size) 16 | for (i in 0 until size) { 17 | res.add(listeners.valueAt(i)) 18 | } 19 | return res 20 | } 21 | } 22 | 23 | fun updateListener(gls: GLocationService, client: Client, key: Any, listener: OsLocationListener) { 24 | val listeners = gls.listeners 25 | synchronized(listeners) { 26 | client.locationManager.requestLocationUpdates(listener.provider.name, listener.request, 27 | gls.listenerCallbacksExecutor, listener) 28 | 29 | val curIdx = listeners.indexOfKey(key) 30 | if (curIdx >= 0) { 31 | removeInternal(listeners.valueAt(curIdx)) 32 | listeners.setValueAt(curIdx, listener) 33 | } else { 34 | listeners.put(key, listener) 35 | if (listeners.size > MAX_COUNT) { 36 | removeListener(listeners, key) 37 | throw IllegalStateException("too many (${listeners.size}) listeners are already registered") 38 | } 39 | } 40 | logd{"client ${client.packageName} ${if (curIdx >= 0) "updated" else "added" } " + 41 | "listener ${key.javaClass}, ${listener.request} provider ${listener.provider} listenerCount ${listeners.size}"} 42 | } 43 | } 44 | 45 | private fun removeInternal(listener: OsLocationListener) { 46 | try { 47 | listener.client.locationManager.removeUpdates(listener) 48 | } catch (e: Throwable) { 49 | Log.e("Listeners", "listener removal should never fail", e) 50 | System.exit(1) 51 | } 52 | } 53 | 54 | fun removeListener(listeners: Listeners, key: Any) { 55 | synchronized(listeners) { 56 | val idx = listeners.indexOfKey(key) 57 | if (idx < 0) { 58 | return 59 | } 60 | val listener = listeners.valueAt(idx) 61 | removeInternal(listener) 62 | listeners.removeAt(idx) 63 | logd{"removed listener ${key.javaClass}, listenerCount ${listeners.size}"} 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/LocationRequestUpdateData.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import android.annotation.Nullable; 4 | import android.app.PendingIntent; 5 | import android.app.appsearch.safeparcel.SafeParcelable; 6 | import android.os.IBinder; 7 | import android.os.Parcelable; 8 | 9 | import com.google.android.gms.location.ILocationCallback; 10 | import com.google.android.gms.location.ILocationListener; 11 | 12 | import com.google.android.gms.RoSafeParcelable; 13 | 14 | @SafeParcelable.Class(creator = "LocationRequestUpdateDataCreator") 15 | public class LocationRequestUpdateData extends RoSafeParcelable { 16 | public static final int OP_REQUEST_UPDATES = 1; 17 | public static final int OP_REMOVE_UPDATES = 2; 18 | 19 | @Field(id = 1) public int opCode; 20 | @Field(id = 2) public LocationRequestInternal request; 21 | @Field(id = 3) public IBinder listener; 22 | @Field(id = 4) public PendingIntent pendingIntent; 23 | @Field(id = 5) public IBinder callback; 24 | @Field(id = 6) public IBinder fusedLocationProviderCallback; 25 | @Field(id = 8) public String appOpsReasonMessage; 26 | 27 | @Constructor 28 | public LocationRequestUpdateData(@Param(id = 1) int opCode, 29 | @Param(id = 2) LocationRequestInternal request, 30 | @Param(id = 3) IBinder listener, 31 | @Param(id = 4) PendingIntent pendingIntent, 32 | @Param(id = 5) IBinder callback, 33 | @Param(id = 6) IBinder fusedLocationProviderCallback, 34 | @Param(id = 8) String appOpsReasonMessage) { 35 | this.opCode = opCode; 36 | this.request = request; 37 | this.listener = listener; 38 | this.pendingIntent = pendingIntent; 39 | this.callback = callback; 40 | this.fusedLocationProviderCallback = fusedLocationProviderCallback; 41 | this.appOpsReasonMessage = appOpsReasonMessage; 42 | } 43 | 44 | @Nullable 45 | public ILocationListener getListener() { 46 | return ILocationListener.Stub.asInterface(listener); 47 | } 48 | 49 | @Nullable 50 | public ILocationCallback getCallback() { 51 | return ILocationCallback.Stub.asInterface(callback); 52 | } 53 | 54 | @Nullable 55 | public IFusedLocationProviderCallback getFusedLocationProviderCallback() { 56 | return IFusedLocationProviderCallback.Stub.asInterface(fusedLocationProviderCallback); 57 | } 58 | 59 | public static final Parcelable.Creator CREATOR = new LocationRequestUpdateDataCreator(); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/sysservice/client/GclPackageManager.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.sysservice.client; 2 | 3 | import android.app.ActivityThread; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.pm.IPackageManager; 6 | import android.content.pm.PackageInfo; 7 | import android.content.pm.PackageManager; 8 | import android.content.pm.ProviderInfo; 9 | import android.content.pm.Signature; 10 | import android.ext.PackageId; 11 | import android.os.IBinder; 12 | import android.os.RemoteException; 13 | import android.util.Log; 14 | 15 | import app.grapheneos.gmscompat.lib.util.Certs; 16 | 17 | class GclPackageManager extends IPackageManager.Stub.Proxy { 18 | static final String TAG = "GclPackageManager"; 19 | 20 | GclPackageManager(IBinder remote) { super(remote); } 21 | 22 | @Override 23 | public ProviderInfo resolveContentProvider(String name, long flags, int userId) throws RemoteException { 24 | ProviderInfo res = super.resolveContentProvider(name, flags, userId); 25 | if (res == null && ShimGmsFontProvider.AUTHORITY.equals(name)) { 26 | Log.d(TAG, "resolveContentProvider: providing ShimGmsFontProvider info"); 27 | return ShimGmsFontProvider.makeProviderInfo(); 28 | } 29 | return res; 30 | } 31 | 32 | @Override 33 | public PackageInfo getPackageInfo(String packageName, long flags, int userId) throws RemoteException { 34 | PackageInfo res = super.getPackageInfo(packageName, flags, userId); 35 | 36 | //noinspection deprecation 37 | if (res == null && PackageId.GMS_CORE_NAME.equals(packageName) && flags == PackageManager.GET_SIGNATURES) { 38 | Log.d(TAG, "getPackageInfo: providing GmsCore cert info"); 39 | var pkgInfo = new PackageInfo(); 40 | //noinspection deprecation 41 | pkgInfo.signatures = new Signature[] { Certs.gmsCore() }; 42 | return pkgInfo; 43 | } 44 | return res; 45 | } 46 | 47 | @Override 48 | public ApplicationInfo getApplicationInfo(String packageName, long flags, int userId) throws RemoteException { 49 | ApplicationInfo res = super.getApplicationInfo(packageName, flags, userId); 50 | if (res == null && PackageId.GMS_CORE_NAME.equals(packageName) && flags == PackageManager.GET_META_DATA) { 51 | Log.d(TAG, "getApplicationInfo: providing empty GmsCore ApplicationInfo"); 52 | /* see font preloading sequence in */ /** @see ActivityThread#handleBindApplication */ 53 | res = new ApplicationInfo(); 54 | } 55 | return res; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/location/GLocationForwarder.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.location 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.location.Location 7 | import android.os.IBinder 8 | import com.google.android.gms.location.ILocationCallback 9 | import com.google.android.gms.location.ILocationListener 10 | import com.google.android.gms.location.LocationAvailability 11 | import com.google.android.gms.location.LocationResult 12 | 13 | abstract class GLocationForwarder { 14 | lateinit var listeners: Listeners 15 | 16 | abstract fun listenerKey(): Any 17 | 18 | abstract fun forwardLocations(ctx: Context, locations: List) 19 | abstract fun onLocationAvailabilityChanged(ctx: Context, la: LocationAvailability) 20 | 21 | fun unregister() { 22 | removeListener(listeners, listenerKey()) 23 | } 24 | } 25 | 26 | class GlfPendingIntent(val pendingIntent: PendingIntent) : GLocationForwarder() { 27 | override fun forwardLocations(ctx: Context, locations: List) { 28 | val intent = Intent() 29 | intent.putExtra("com.google.android.gms.location.EXTRA_LOCATION_RESULT", LocationResult(locations)) 30 | // intent.putExtra("com.google.android.location.LOCATION", locations.get(locations.size - 1)) 31 | pendingIntent.send(ctx, 0, intent) 32 | } 33 | 34 | override fun onLocationAvailabilityChanged(ctx: Context, la: LocationAvailability) { 35 | val intent = Intent() 36 | intent.putExtra("com.google.android.gms.location.EXTRA_LOCATION_AVAILABILITY", la) 37 | pendingIntent.send(ctx, 0, intent) 38 | } 39 | 40 | override fun listenerKey(): Any = pendingIntent 41 | } 42 | 43 | abstract class GlfBinder(val binder: IBinder) : GLocationForwarder() { 44 | override fun listenerKey(): Any = binder 45 | } 46 | 47 | class GlfLocationCallback(val callback: ILocationCallback) : GlfBinder(callback.asBinder()) { 48 | override fun forwardLocations(ctx: Context, locations: List) { 49 | val lr = LocationResult(locations) 50 | callback.onLocationResult(lr) 51 | } 52 | 53 | override fun onLocationAvailabilityChanged(ctx: Context, la: LocationAvailability) { 54 | callback.onLocationAvailability(la) 55 | } 56 | } 57 | 58 | class GlfLocationListener(val listener: ILocationListener) : GlfBinder(listener.asBinder()) { 59 | override fun forwardLocations(ctx: Context, locations: List) { 60 | // same behavior as GmsCore 61 | locations.forEach { 62 | listener.onLocationChanged(it) 63 | } 64 | } 65 | 66 | override fun onLocationAvailabilityChanged(ctx: Context, la: LocationAvailability) { 67 | // ILocationListener doesn't have a corresponding callback 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location.internal; 2 | 3 | import android.app.PendingIntent; 4 | import android.location.Location; 5 | import android.os.Bundle; 6 | 7 | import com.google.android.gms.common.api.Status; 8 | import com.google.android.gms.common.api.internal.IStatusCallback; 9 | import com.google.android.gms.common.internal.ICancelToken; 10 | import com.google.android.gms.location.CurrentLocationRequest; 11 | import com.google.android.gms.location.LastLocationRequest; 12 | import com.google.android.gms.location.LocationAvailability; 13 | import com.google.android.gms.location.LocationAvailabilityRequest; 14 | import com.google.android.gms.location.LocationRequest; 15 | import com.google.android.gms.location.LocationSettingsRequest; 16 | import com.google.android.gms.location.ILocationListener; 17 | import com.google.android.gms.location.internal.IBooleanStatusCallback; 18 | import com.google.android.gms.location.internal.IFusedLocationProviderCallback; 19 | import com.google.android.gms.location.internal.ILocationStatusCallback; 20 | import com.google.android.gms.location.internal.ISettingsCallbacks; 21 | import com.google.android.gms.location.internal.LocationReceiver; 22 | import com.google.android.gms.location.internal.LocationRequestInternal; 23 | import com.google.android.gms.location.internal.LocationRequestUpdateData; 24 | 25 | interface IGoogleLocationManagerService { 26 | Location getLastLocation() = 6; 27 | Location getLastLocation2(String packageName) = 20; 28 | Location getLastLocation3(String contextAttributionTag) = 79; 29 | void getLastLocation4(in LastLocationRequest request, ILocationStatusCallback callback) = 81; 30 | void getLastLocation5(in LastLocationRequest request, in LocationReceiver receiver) = 89; 31 | 32 | LocationAvailability getLocationAvailability(String packageName) = 33; 33 | void getLocationAvailability2(in LocationAvailabilityRequest request, in LocationReceiver receiver) = 90; 34 | 35 | void checkLocationSettings(in LocationSettingsRequest settingsRequest, ISettingsCallbacks callback, String packageName) = 62; 36 | 37 | void updateLocationRequest(in LocationRequestUpdateData locationRequestUpdateData) = 58; 38 | void flushLocations(IFusedLocationProviderCallback callback) = 66; 39 | 40 | void registerLocationReceiver(in LocationReceiver receiver, in LocationRequest request, IStatusCallback callback) = 87; 41 | void unregisterLocationReceiver(in LocationReceiver receiver, IStatusCallback callback) = 88; 42 | 43 | ICancelToken getCurrentLocation(in CurrentLocationRequest request, ILocationStatusCallback callback) = 86; 44 | ICancelToken getCurrentLocation2(in CurrentLocationRequest request, in LocationReceiver receiver) = 91; 45 | 46 | void isGoogleLocationAccuracyEnabled(IBooleanStatusCallback callback) = 94; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/App.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat; 2 | 3 | import android.app.Application; 4 | import android.app.NotificationManager; 5 | import android.content.Context; 6 | import android.content.SharedPreferences; 7 | 8 | import com.android.internal.gmscompat.GmsCompatApp; 9 | 10 | import java.util.concurrent.Executor; 11 | import java.util.concurrent.Executors; 12 | 13 | public class App extends Application { 14 | private static Context ctx; 15 | private static Context deviceProtectedStorageContext; 16 | private static NotificationManager notificationManager; 17 | private static SharedPreferences preferences; 18 | private static Thread mainThread; 19 | private static Executor bgExecutor; 20 | 21 | public void onCreate() { 22 | super.onCreate(); 23 | maybeInit(this); 24 | } 25 | 26 | static void maybeInit(Context componentContext) { 27 | if (ctx != null) { 28 | return; 29 | } 30 | ctx = componentContext.getApplicationContext(); 31 | mainThread = Thread.currentThread(); 32 | bgExecutor = Executors.newSingleThreadExecutor(); 33 | 34 | if (GmsCompatApp.PKG_NAME.equals(Application.getProcessName())) { 35 | // main process 36 | deviceProtectedStorageContext = ctx.createDeviceProtectedStorageContext(); 37 | preferences = deviceProtectedStorageContext 38 | .getSharedPreferences(MAIN_PROCESS_PREFS_FILE, MODE_PRIVATE); 39 | notificationManager = ctx.getSystemService(NotificationManager.class); 40 | Notifications.createNotificationChannels(); 41 | 42 | new ConfigUpdateReceiver(ctx); 43 | } 44 | } 45 | 46 | public static Context ctx() { 47 | return ctx; 48 | } 49 | 50 | public static Context deviceProtectedStorageContext() { 51 | return deviceProtectedStorageContext; 52 | } 53 | 54 | public static NotificationManager notificationManager() { 55 | return notificationManager; 56 | } 57 | 58 | public static SharedPreferences preferences() { 59 | return preferences; 60 | } 61 | 62 | public static Thread mainThread() { 63 | return mainThread; 64 | } 65 | 66 | public static Executor bgExecutor() { 67 | return bgExecutor; 68 | } 69 | 70 | private static final String MAIN_PROCESS_PREFS_FILE = "prefs"; 71 | 72 | public interface MainProcessPrefs { 73 | String LOCATION_REQUEST_REDIRECTION_ENABLED = "enabled_redirections"; // historical name 74 | 75 | String GmsCore_POWER_EXEMPTION_PROMPT_DISMISSED = "GmsCore_power_exemption_prompt_dismissed_2"; 76 | String GmsCore_BACKGROUND_DATA_EXEMPTION_PROMPT_DISMISSED = "GmsCore_background_data_exemption_prompt_dismissed"; 77 | String NOTIFICATION_DO_NOT_SHOW_AGAIN_PREFIX = "do_not_show_notification_"; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/sysservice/client/ShimGmsFontProvider.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.sysservice.client; 2 | 3 | import android.annotation.NonNull; 4 | import android.annotation.Nullable; 5 | import android.app.compat.gms.GmsCompat; 6 | import android.content.AttributionSource; 7 | import android.content.pm.ProviderInfo; 8 | import android.content.res.AssetFileDescriptor; 9 | import android.database.Cursor; 10 | import android.database.MatrixCursor; 11 | import android.ext.PackageId; 12 | import android.net.Uri; 13 | import android.os.Bundle; 14 | import android.os.ICancellationSignal; 15 | import android.os.ParcelFileDescriptor; 16 | import android.os.RemoteException; 17 | import android.util.Log; 18 | 19 | import java.io.File; 20 | import java.io.FileNotFoundException; 21 | import java.util.Arrays; 22 | 23 | import app.grapheneos.gmscompat.lib.util.BaseIContentProvider; 24 | 25 | // This provider prevents crashes of apps that depend on GmsCore font provider when GmsCore is 26 | // missing or disabled. 27 | public class ShimGmsFontProvider extends BaseIContentProvider { 28 | static final String TAG = "ShimGmsFontProvider"; 29 | static final String AUTHORITY = "com.google.android.gms.fonts"; 30 | 31 | static ProviderInfo makeProviderInfo() { 32 | var res = new ProviderInfo(); 33 | res.packageName = PackageId.GMS_CORE_NAME; 34 | res.name = AUTHORITY; 35 | res.authority = AUTHORITY; 36 | res.applicationInfo = GmsCompat.appContext().getApplicationInfo(); 37 | res.applicationInfo.packageName = res.packageName; 38 | return res; 39 | } 40 | 41 | // randomly generated ID 42 | private static final String FONT_FILE_ID = "463606443"; 43 | 44 | @Override 45 | public Cursor query(@NonNull AttributionSource attributionSource, Uri url, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable ICancellationSignal cancellationSignal) throws RemoteException { 46 | Log.d(TAG, "query: " + url.toString() + " projection: " + Arrays.toString(projection) + " args: " + (queryArgs != null ? queryArgs.toStringDeep() : "")); 47 | var cursor = new MatrixCursor(new String[] { "file_id" }); 48 | cursor.addRow(new Object[] { FONT_FILE_ID }); 49 | return cursor; 50 | } 51 | 52 | @Override 53 | public AssetFileDescriptor openTypedAssetFile(@NonNull AttributionSource attributionSource, Uri url, String mimeType, Bundle opts, ICancellationSignal signal) throws RemoteException, FileNotFoundException { 54 | Log.d(TAG, "openTypedAssetFile: " + url + " " + url.getPath() + " mimeType: " + mimeType + " opts: " + (opts != null ? opts.toStringDeep() : "")); 55 | if (!("/file/" + FONT_FILE_ID).equals(url.getPath())) { 56 | throw new FileNotFoundException(); 57 | } 58 | var file = new File("/system/fonts/Roboto-Regular.ttf"); 59 | var pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 60 | return new AssetFileDescriptor(pfd, 0L, AssetFileDescriptor.UNKNOWN_LENGTH); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/sysservice/client/GclActivityManager.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.sysservice.client; 2 | 3 | import android.app.AppGlobals; 4 | import android.app.ContentProviderHolder; 5 | import android.app.IActivityManager; 6 | import android.app.IApplicationThread; 7 | import android.app.Notification; 8 | import android.content.ComponentName; 9 | import android.content.Intent; 10 | import android.ext.PackageId; 11 | import android.os.IBinder; 12 | import android.os.RemoteException; 13 | import android.util.Log; 14 | 15 | class GclActivityManager extends IActivityManager.Stub.Proxy { 16 | static final String TAG = "GclActivityManager"; 17 | 18 | GclActivityManager(IBinder remote) { super(remote); } 19 | 20 | @Override 21 | public ContentProviderHolder getContentProvider(IApplicationThread caller, String callingPackage, String name, int userId, boolean stable) throws RemoteException { 22 | ContentProviderHolder res = super.getContentProvider(caller, callingPackage, name, userId, stable); 23 | if (res == null && ShimGmsFontProvider.AUTHORITY.equals(name)) { 24 | Log.d(TAG, "getContentProvider: returning ShimGmsFontProvider"); 25 | var cph = new ContentProviderHolder(ShimGmsFontProvider.makeProviderInfo()); 26 | cph.provider = new ShimGmsFontProvider(); 27 | cph.noReleaseNeeded = true; 28 | cph.mLocal = true; 29 | return cph; 30 | } 31 | return res; 32 | } 33 | 34 | /** @see android.app.ContextImpl#startServiceCommon */ 35 | @Override 36 | public ComponentName startService(IApplicationThread caller, Intent service, String resolvedType, boolean requireForeground, String callingPackage, String callingFeatureId, int userId) throws RemoteException { 37 | ComponentName res = super.startService(caller, service, resolvedType, requireForeground, callingPackage, callingFeatureId, userId); 38 | if (AppGlobals.getInitialPackageId() == PackageId.G_CAMERA) { 39 | if (res != null && "?".equals(res.getPackageName()) && service.getComponent() != null && service.getComponent().getClassName().endsWith(".NoOpPrewarmService")) { 40 | // NoOpPrewarmService is a performance optimization. GoogleCamera starts it from 41 | // background in some cases, which leads to a BackgroundServiceStartNotAllowedException 42 | // chain-crash. 43 | // 44 | // GoogleCamera has privileged integration on stock OS, it's always allowed to 45 | // start background services there. 46 | Log.d(TAG, "ignoring failed start of NoOpPrewarmService in GoogleCamera"); 47 | return null; 48 | } 49 | } 50 | return res; 51 | } 52 | 53 | @Override 54 | public void setServiceForeground(ComponentName className, IBinder token, int id, Notification notification, int flags, int foregroundServiceType) throws android.os.RemoteException { 55 | try { 56 | super.setServiceForeground(className, token, id, notification, flags, foregroundServiceType); 57 | } catch (SecurityException e) { 58 | Log.d(TAG, "ignoring setServiceForeground", e); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config-holder/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.playintegrity; 2 | 3 | import android.annotation.Nullable; 4 | import android.content.Context; 5 | import android.content.pm.GosPackageState; 6 | import android.content.pm.GosPackageStateFlag; 7 | import android.ext.settings.app.AswBlockPlayIntegrityApi; 8 | import android.os.Binder; 9 | import android.os.BinderWrapper; 10 | import android.os.IBinder; 11 | import android.os.Parcel; 12 | import android.os.RemoteException; 13 | import android.util.Log; 14 | 15 | import com.android.internal.gmscompat.GmsCompatApp; 16 | import com.android.internal.os.BackgroundThread; 17 | 18 | import static android.app.compat.gms.GmsCompat.appContext; 19 | import static app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils.isPlayIntegrityBlocked; 20 | 21 | abstract class PlayIntegrityServiceWrapper extends BinderWrapper { 22 | final String TAG; 23 | protected int requestIntegrityTokenTxnCode; 24 | 25 | public PlayIntegrityServiceWrapper(IBinder base) { 26 | super(base); 27 | TAG = getClass().getSimpleName(); 28 | } 29 | 30 | protected abstract Binder createTokenRequestStub(); 31 | 32 | @Override 33 | public boolean transact(int code, Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { 34 | if (code == requestIntegrityTokenTxnCode) { 35 | if (maybeStubOutIntegrityTokenRequest(code, data, reply, flags)) { 36 | return true; 37 | } 38 | } 39 | return super.transact(code, data, reply, flags); 40 | } 41 | 42 | private void onIntegrityTokenRequest(boolean isBlocked) { 43 | Runnable r = () -> { 44 | Context ctx = appContext(); 45 | GosPackageState gosPs = GosPackageState.getForSelf(ctx); 46 | if (!gosPs.hasFlag(GosPackageStateFlag.PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE)) { 47 | gosPs.createEditor(ctx.getPackageName(), ctx.getUser()) 48 | .addFlag(GosPackageStateFlag.PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE) 49 | .apply(); 50 | } 51 | if (!AswBlockPlayIntegrityApi.I.isNotificationEnabled(gosPs)) { 52 | return; 53 | } 54 | try { 55 | GmsCompatApp.iClientOfGmsCore2Gca().showPlayIntegrityNotification(ctx.getPackageName(), isBlocked); 56 | } catch (RemoteException e) { 57 | Log.e(TAG, "", e); 58 | } 59 | }; 60 | BackgroundThread.getHandler().post(r); 61 | } 62 | 63 | private boolean maybeStubOutIntegrityTokenRequest(int code, Parcel data, @Nullable Parcel reply, int flags) { 64 | Log.d(TAG, "integrity token request detected"); 65 | 66 | boolean isBlocked = isPlayIntegrityBlocked(); 67 | onIntegrityTokenRequest(isBlocked); 68 | 69 | if (!isBlocked) { 70 | return false; 71 | } 72 | 73 | try { 74 | createTokenRequestStub().transact(code, data, reply, flags); 75 | } catch (RemoteException e) { 76 | // this is a local call 77 | throw new IllegalStateException(e); 78 | } 79 | return true; 80 | } 81 | 82 | protected static long getTokenRequestResultDelay() { 83 | return 500L; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/GmsCompatLibImpl.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib; 2 | 3 | import android.annotation.Nullable; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.os.BinderProxy; 8 | import android.os.IBinder; 9 | import android.os.IInterface; 10 | import android.os.UserHandle; 11 | import android.util.ArrayMap; 12 | import android.util.Log; 13 | 14 | import com.android.internal.gmscompat.IGmsCompatLib; 15 | 16 | import java.io.FileDescriptor; 17 | import java.util.Arrays; 18 | import java.util.function.Function; 19 | 20 | import app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils; 21 | import app.grapheneos.gmscompat.lib.sysservice.SystemServiceOverridesRegistry; 22 | import app.grapheneos.gmscompat.lib.util.BinderUtils; 23 | 24 | /** 25 | * GmsCompatLibrary code is loaded into processes of apps that use GmsCompat. 26 | */ 27 | public class GmsCompatLibImpl implements IGmsCompatLib { 28 | private static final String TAG = "GmcLib"; 29 | 30 | @Override 31 | public void init(Context appContext, Context libContext, String processName) { 32 | Log.d(TAG, "init: pkg: " + appContext.getPackageName() + ", process: " + processName); 33 | SystemServiceOverridesRegistry.init(appContext, binderProxyOverridesRegistry); 34 | } 35 | 36 | @Override 37 | public ServiceConnection maybeReplaceServiceConnection(Intent service, long flags, UserHandle user, ServiceConnection orig) { 38 | ServiceConnection override = PlayIntegrityUtils.maybeReplaceServiceConnection(service, orig); 39 | return override; 40 | } 41 | 42 | private static final String TAG_BINDER = "GmcLibBinder"; 43 | private final ArrayMap> binderProxyOverridesRegistry = new ArrayMap<>(); 44 | 45 | @Nullable 46 | @Override 47 | public IInterface maybeProvideBinderProxyInterface(BinderProxy binderProxy, String ifaceDescriptor) { 48 | if (Log.isLoggable(TAG_BINDER, Log.DEBUG)) { 49 | Log.d(TAG_BINDER, "maybeProvideBinderProxyInterface: " + ifaceDescriptor + " | " + BinderUtils.getInterfaceDescriptor(binderProxy), maybeStackTrace(TAG_BINDER)); 50 | } 51 | 52 | Function creator = binderProxyOverridesRegistry.get(ifaceDescriptor); 53 | if (creator == null) { 54 | return null; 55 | } 56 | String actualIfaceDescriptor = BinderUtils.getInterfaceDescriptor(binderProxy); 57 | if (!ifaceDescriptor.equals(actualIfaceDescriptor)) { 58 | Log.w(TAG, "interface descriptor mismatch: expected " + ifaceDescriptor + " got " + actualIfaceDescriptor); 59 | return null; 60 | } 61 | return creator.apply(binderProxy); 62 | } 63 | 64 | @Override 65 | public boolean maybeInterceptBinderProxyDump(BinderProxy binderProxy, FileDescriptor fd, String[] args, boolean async) { 66 | if (Log.isLoggable(TAG_BINDER, Log.DEBUG)) { 67 | Log.d(TAG_BINDER, "maybeInterceptBinderProxyDump: " + BinderUtils.getInterfaceDescriptor(binderProxy) + " " + Arrays.toString(args), maybeStackTrace(TAG_BINDER)); 68 | } 69 | return false; 70 | } 71 | 72 | @Nullable 73 | private static Throwable maybeStackTrace(String tag) { 74 | return Log.isLoggable(tag, Log.VERBOSE) ? new Throwable() : null; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/NotableInterface.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.Manifest 4 | import android.app.ActivityManager 5 | import android.content.Context 6 | import app.grapheneos.gmscompat.Const.IS_DEV_BUILD 7 | import com.android.internal.gmscompat.GmsInfo 8 | 9 | enum class NotableInterface(val ifaceName: String) { 10 | ExposureNotificationService("com.google.android.gms.nearby.exposurenotification.internal.INearbyExposureNotificationService"), 11 | GamesService("com.google.android.gms.games.internal.IGamesService"), 12 | WearableService("com.google.android.gms.wearable.internal.IWearableService"), 13 | ; 14 | 15 | fun onAcquiredByClient(callerPkg: String, processState: Int) { 16 | if (IS_DEV_BUILD) { 17 | logd{"pkgName $callerPkg, processState: ${ActivityManager.procStateToString(processState)}, ifaceName $ifaceName"} 18 | } 19 | 20 | val ctx = App.ctx() 21 | when (this) { 22 | ExposureNotificationService -> { 23 | if (processState > ActivityManager.PROCESS_STATE_TOP) { 24 | return 25 | } 26 | 27 | if (!gmsCoreHasPermission(Manifest.permission.BLUETOOTH_SCAN)) { 28 | Notifications.configurationRequired( 29 | Notifications.CH_MISSING_PERMISSION, 30 | ctx.getText(R.string.missing_permission), 31 | ctx.getText(R.string.missing_permission_nearby_exposurenotifications), 32 | ctx.getText(R.string.open_settings), 33 | appSettingsIntent(GmsInfo.PACKAGE_GMS_CORE, APP_INFO_ITEM_PERMISSIONS) 34 | ).show(Notifications.ID_GMS_CORE_MISSING_NEARBY_DEVICES_PERMISSION) 35 | } 36 | } 37 | GamesService -> { 38 | Notifications.handleMissingApp(Notifications.CH_MISSING_PLAY_GAMES_APP, 39 | ctx.getString(R.string.missing_play_games_app, getApplicationLabel(ctx, callerPkg)), 40 | "com.google.android.play.games") 41 | } 42 | WearableService -> { 43 | if (processState > ActivityManager.PROCESS_STATE_TOP) { 44 | return 45 | } 46 | 47 | // this service is acquired by many Google apps in background and in foreground, 48 | // show notif only for foreground Wear OS companion apps 49 | when (callerPkg) { 50 | // "Wear OS" 51 | "com.google.android.wearable.app", 52 | // "Google Pixel Watch" 53 | "com.google.android.apps.wear.companion", 54 | -> showGmsCoreMissingNearbyDevicesPermGeneric(ctx, callerPkg) 55 | } 56 | 57 | } 58 | } 59 | } 60 | 61 | fun showGmsCoreMissingNearbyDevicesPermGeneric(ctx: Context, callerPkg: String) { 62 | Notifications.configurationRequired( 63 | Notifications.CH_MISSING_PERMISSION, 64 | ctx.getText(R.string.missing_permission), 65 | ctx.getString(R.string.notif_GmsCore_missing_nearby_devices_perm_generic, getApplicationLabel(ctx, callerPkg)), 66 | ctx.getText(R.string.open_settings), 67 | appSettingsIntent(GmsInfo.PACKAGE_GMS_CORE, APP_INFO_ITEM_PERMISSIONS) 68 | ).show(Notifications.ID_GMS_CORE_MISSING_NEARBY_DEVICES_PERMISSION) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/com/google/android/gms/location/LocationRequest.java: -------------------------------------------------------------------------------- 1 | package com.google.android.gms.location; 2 | 3 | import android.app.appsearch.safeparcel.SafeParcelable; 4 | import android.os.Parcelable; 5 | import android.os.WorkSource; 6 | 7 | import com.google.android.gms.RoSafeParcelable; 8 | 9 | import app.grapheneos.gmscompat.UtilsKt; 10 | 11 | // https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest 12 | @SafeParcelable.Class(creator = "LocationRequestCreator") 13 | public class LocationRequest extends RoSafeParcelable { 14 | public static final int PRIORITY_BALANCED_POWER_ACCURACY = 102; 15 | public static final int PRIORITY_HIGH_ACCURACY = 100; 16 | public static final int PRIORITY_LOW_POWER = 104; 17 | public static final int PRIORITY_NO_POWER = 105; 18 | 19 | // https://developers.google.com/android/reference/com/google/android/gms/location/Granularity 20 | public static final int GRANULARITY_PERMISSION_LEVEL = 0; 21 | public static final int GRANULARITY_COARSE = 1; 22 | public static final int GRANULARITY_FINE = 2; 23 | 24 | public static final int THROTTLE_BACKGROUND = 0; 25 | public static final int THROTTLE_ALWAYS = 1; 26 | public static final int THROTTLE_NEVER = 2; 27 | 28 | @Field(id = 1) public int priority; 29 | @Field(id = 2) public long interval; 30 | @Field(id = 3) public long minUpdateIntervalMillis; 31 | @Field(id = 5, defaultValueUnchecked = "Long.MAX_VALUE") public long expirationTime; 32 | @Field(id = 6) public int maxUpdates; 33 | @Field(id = 7) public float minUpdateDistanceMeters; 34 | @Field(id = 8) public long maxUpdateDelayMillis; 35 | @Field(id = 9) public boolean waitForAccurateLocation; 36 | @Field(id = 10, defaultValueUnchecked = "Long.MAX_VALUE") public long durationMillis; 37 | @Field(id = 11, defaultValue = "-1") public long maxUpdateAgeMillis; 38 | @Field(id = 12, defaultValueUnchecked = "LocationRequest.GRANULARITY_PERMISSION_LEVEL") public int granularity; 39 | @Field(id = 13) public int throttleBehavior; 40 | @Field(id = 15) public boolean bypass; 41 | @Field(id = 16) public WorkSource workSource; 42 | 43 | @Constructor 44 | public LocationRequest(@Param(id = 1) int priority, 45 | @Param(id = 2) long interval, 46 | @Param(id = 3) long minUpdateIntervalMillis, 47 | @Param(id = 5) long expirationTime, 48 | @Param(id = 6) int maxUpdates, 49 | @Param(id = 7) float minUpdateDistanceMeters, 50 | @Param(id = 8) long maxUpdateDelayMillis, 51 | @Param(id = 9) boolean waitForAccurateLocation, 52 | @Param(id = 10) long durationMillis, 53 | @Param(id = 11) long maxUpdateAgeMillis, 54 | @Param(id = 12) int granularity, 55 | @Param(id = 13) int throttleBehavior, 56 | @Param(id = 15) boolean bypass, 57 | @Param(id = 16) WorkSource workSource 58 | ) { 59 | this.priority = priority; 60 | this.interval = interval; 61 | this.minUpdateIntervalMillis = minUpdateIntervalMillis; 62 | this.expirationTime = expirationTime; 63 | this.maxUpdates = maxUpdates; 64 | this.minUpdateDistanceMeters = minUpdateDistanceMeters; 65 | this.maxUpdateDelayMillis = maxUpdateDelayMillis; 66 | this.waitForAccurateLocation = waitForAccurateLocation; 67 | this.durationMillis = durationMillis; 68 | this.maxUpdateAgeMillis = maxUpdateAgeMillis; 69 | this.granularity = granularity; 70 | this.throttleBehavior = throttleBehavior; 71 | this.bypass = bypass; 72 | this.workSource = workSource; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return UtilsKt.objectToString(this); 78 | } 79 | 80 | public static final Parcelable.Creator CREATOR = new LocationRequestCreator(); 81 | } 82 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/TempServiceBinding.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat; 2 | 3 | import android.annotation.Nullable; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.os.IBinder; 9 | import android.os.PowerExemptionManager; 10 | import android.util.Log; 11 | 12 | import com.android.internal.gmscompat.client.GmsCompatClientService; 13 | import com.android.internal.infra.AndroidFuture; 14 | import com.android.internal.os.BackgroundThread; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicLong; 18 | 19 | public class TempServiceBinding implements ServiceConnection, Runnable { 20 | private static final AtomicLong idSrc = new AtomicLong(); 21 | 22 | private final String TAG = TempServiceBinding.class.getSimpleName() + ':' + idSrc.getAndIncrement(); 23 | 24 | private final AndroidFuture connectionResult = new AndroidFuture<>(); 25 | 26 | private TempServiceBinding() {} 27 | 28 | public static void create(String targetPkg, long durationMs, @Nullable String reason, int reasonCode) { 29 | if (durationMs <= 0) { 30 | throw new IllegalArgumentException(Long.toString(durationMs)); 31 | } 32 | 33 | var instance = new TempServiceBinding(); 34 | String TAG = instance.TAG; 35 | 36 | Log.d(TAG, "create: pkgName " + targetPkg 37 | + ", duration: " + durationMs 38 | + ", reason: " + reason 39 | + ", reasonCode: " + PowerExemptionManager.reasonCodeToString(reasonCode)); 40 | 41 | var intent = new Intent(); 42 | intent.setClassName(targetPkg, GmsCompatClientService.class.getName()); 43 | 44 | Context appContext = App.ctx(); 45 | if (!appContext.bindService(intent, Context.BIND_AUTO_CREATE, BackgroundThread.getExecutor(), instance)) { 46 | Log.e(TAG, "bindService() returned false"); 47 | appContext.unbindService(instance); 48 | return; 49 | } 50 | 51 | BackgroundThread.getHandler().postDelayed(instance, durationMs); 52 | 53 | try { 54 | Boolean res = instance.connectionResult.get(5, TimeUnit.SECONDS); 55 | if (!res.booleanValue()) { 56 | Log.e(TAG, "connectionResult is false"); 57 | } 58 | } catch (Exception e) { 59 | Log.e(TAG, "", e); 60 | } 61 | 62 | if (Log.isLoggable(TAG, Log.VERBOSE)) { 63 | Log.d(TAG, "onConnected latch completed"); 64 | } 65 | } 66 | 67 | @Override 68 | public void run() { 69 | Log.d(TAG, "timeout expired, unbinding"); 70 | unbind(); 71 | } 72 | 73 | private boolean unbound; 74 | 75 | private void unbind() { 76 | if (!connectionResult.isDone()) { 77 | connectionResult.complete(Boolean.FALSE); 78 | } 79 | 80 | if (unbound) { 81 | Log.d(TAG, "already unbound"); 82 | } else { 83 | App.ctx().unbindService(this); 84 | Log.d(TAG, "unbind"); 85 | unbound = true; 86 | } 87 | } 88 | 89 | @Override 90 | public void onServiceConnected(ComponentName name, IBinder service) { 91 | if (Log.isLoggable(TAG, Log.VERBOSE)) { 92 | Log.v(TAG, "onServiceConnected"); 93 | } 94 | connectionResult.complete(Boolean.TRUE); 95 | } 96 | 97 | @Override 98 | public void onServiceDisconnected(ComponentName name) { 99 | Log.e(TAG, "onServiceDisconnected"); 100 | } 101 | 102 | @Override 103 | public void onBindingDied(ComponentName name) { 104 | Log.d(TAG, "onBindingDied"); 105 | unbind(); 106 | } 107 | 108 | @Override 109 | public void onNullBinding(ComponentName name) { 110 | Log.e(TAG, "onNullBinding"); 111 | unbind(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/PendingActionReceiver.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.BroadcastReceiver; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.SharedPreferences; 9 | import android.content.pm.GosPackageState; 10 | import android.content.pm.GosPackageStateFlag; 11 | import android.os.Bundle; 12 | 13 | import java.util.UUID; 14 | 15 | public class PendingActionReceiver extends BroadcastReceiver { 16 | private static final String ACTION_SET_PREF_BOOLEAN_AND_CANCEL_NOTIF = "set_pref_boolean_and_cancel_notif"; 17 | private static final String ACTION_WRITE_GOS_PACKAGE_STATE_AND_CANCEL_NOTIF = "write_gosps_and_cancel_notif"; 18 | private static final String EXTRA_BOOL_PREF_KEY = "bool_pref_key"; 19 | private static final String EXTRA_BOOL_PREF_VALUE = "bool_pref_value"; 20 | private static final String EXTRA_GOS_PACKAGE_STATE_FLAG = "gosps_flag"; 21 | private static final String EXTRA_GOS_PACKAGE_STATE_FLAG_VALUE = "gosps_flag_value"; 22 | private static final String EXTRA_NOTIF_ID = "notif_id"; 23 | 24 | public static PendingIntent makeWriteBoolPrefAndCancelNotif(Context ctx, String key, boolean val, int notifId) { 25 | var i = new Intent(ACTION_SET_PREF_BOOLEAN_AND_CANCEL_NOTIF); 26 | i.putExtra(EXTRA_BOOL_PREF_KEY, key); 27 | i.putExtra(EXTRA_BOOL_PREF_VALUE, val); 28 | i.putExtra(EXTRA_NOTIF_ID, notifId); 29 | return getPendingIntent(ctx, i, PendingIntent.FLAG_ONE_SHOT); 30 | } 31 | 32 | public static PendingIntent makeWriteGosPackageStateAndCancelNotif( 33 | Context ctx, String pkgName, 34 | @GosPackageStateFlag.Enum int gosPsFlag, boolean gosPsFlagValue, int notifId) { 35 | var i = new Intent(ACTION_WRITE_GOS_PACKAGE_STATE_AND_CANCEL_NOTIF); 36 | i.putExtra(Intent.EXTRA_PACKAGE_NAME, pkgName); 37 | i.putExtra(EXTRA_GOS_PACKAGE_STATE_FLAG, gosPsFlag); 38 | i.putExtra(EXTRA_GOS_PACKAGE_STATE_FLAG_VALUE, gosPsFlagValue); 39 | i.putExtra(EXTRA_NOTIF_ID, notifId); 40 | return getPendingIntent(ctx, i, PendingIntent.FLAG_ONE_SHOT); 41 | } 42 | 43 | private static PendingIntent getPendingIntent(Context ctx, Intent intent, int extraFlags) { 44 | // distinctness check ignores intent extras, need to use a unique identifier instead 45 | intent.setIdentifier(UUID.randomUUID().toString()); 46 | intent.setComponent(new ComponentName(ctx, PendingActionReceiver.class)); 47 | return PendingIntent.getBroadcast(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE | extraFlags); 48 | } 49 | 50 | @Override 51 | public void onReceive(Context context, Intent intent) { 52 | Bundle extras = intent.getExtras(); 53 | switch (intent.getAction()) { 54 | case ACTION_SET_PREF_BOOLEAN_AND_CANCEL_NOTIF -> { 55 | String key = extras.getString(EXTRA_BOOL_PREF_KEY); 56 | boolean value = extras.getBoolean2(EXTRA_BOOL_PREF_VALUE); 57 | SharedPreferences.Editor ed = App.preferences().edit(); 58 | ed.putBoolean(key, value); 59 | ed.apply(); 60 | int notifId = extras.getNumber(EXTRA_NOTIF_ID); 61 | Notifications.cancel(notifId); 62 | } 63 | case ACTION_WRITE_GOS_PACKAGE_STATE_AND_CANCEL_NOTIF -> { 64 | String pkgName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 65 | @GosPackageStateFlag.Enum int gosPsFlag = extras.getNumber(EXTRA_GOS_PACKAGE_STATE_FLAG); 66 | boolean gosPsFlagValue = extras.getBoolean2(EXTRA_GOS_PACKAGE_STATE_FLAG_VALUE); 67 | 68 | GosPackageState.Editor ed = GosPackageState.edit(pkgName, context.getUser()); 69 | ed.setFlagState(gosPsFlag, gosPsFlagValue); 70 | ed.apply(); 71 | 72 | int notifId = extras.getNumber(EXTRA_NOTIF_ID); 73 | Notifications.cancel(notifId); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/location/OsLocationProvider.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.location 2 | 3 | import android.location.Location 4 | import android.location.LocationManager 5 | import android.location.provider.ProviderProperties 6 | import com.android.server.location.fudger.LocationFudger 7 | import com.google.android.gms.location.LocationRequest 8 | 9 | class OsLocationProvider(val name: String, val properties: ProviderProperties?, val fudger: LocationFudger?, val permission: Permission) { 10 | 11 | fun maybeFudge(location: Location): Location { 12 | if (fudger == null) { 13 | return location 14 | } 15 | 16 | return fudger.createCoarse(location) 17 | } 18 | 19 | companion object { 20 | fun get(client: Client, req: LocationRequest) = get(client, req.priority, req.granularity) 21 | 22 | fun get(client: Client, priority: Int, granularity: Int): OsLocationProvider { 23 | val name = if (priority == LocationRequest.PRIORITY_NO_POWER) { 24 | LocationManager.PASSIVE_PROVIDER 25 | } else if (priority == LocationRequest.PRIORITY_LOW_POWER) { 26 | if (client.locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { 27 | // GmsCore location service doesn't use GNSS for low-power requests 28 | LocationManager.NETWORK_PROVIDER 29 | } else { 30 | // fused provider doesn't use GNSS for low-power requests, i.e. it doesn't 31 | // provide any locations for low-power requests when network location is off 32 | LocationManager.GPS_PROVIDER 33 | } 34 | } else { 35 | LocationManager.FUSED_PROVIDER 36 | } 37 | return get(client, name, granularity) 38 | } 39 | 40 | fun getPassive(client: Client, granularity: Int): OsLocationProvider { 41 | return get(client, LocationManager.PASSIVE_PROVIDER, granularity) 42 | } 43 | 44 | fun get(client: Client, name: String, granularity: Int): OsLocationProvider { 45 | val properties: ProviderProperties? = client.locationManager.getProviderProperties(name) 46 | 47 | var permission = client.permission 48 | 49 | val fudger: LocationFudger? = when (client.permission) { 50 | Permission.COARSE -> { 51 | when (granularity) { 52 | LocationRequest.GRANULARITY_COARSE, 53 | LocationRequest.GRANULARITY_PERMISSION_LEVEL, -> null 54 | LocationRequest.GRANULARITY_FINE -> throw SecurityException() 55 | else -> throw IllegalArgumentException() 56 | } 57 | } 58 | Permission.FINE -> { 59 | when (granularity) { 60 | LocationRequest.GRANULARITY_COARSE -> { 61 | permission = Permission.COARSE 62 | if (properties == null) { 63 | createLocationFudger() 64 | } else when (properties.accuracy) { 65 | ProviderProperties.ACCURACY_FINE -> createLocationFudger() 66 | ProviderProperties.ACCURACY_COARSE -> null 67 | else -> throw IllegalStateException() 68 | } 69 | } 70 | LocationRequest.GRANULARITY_PERMISSION_LEVEL, 71 | LocationRequest.GRANULARITY_FINE -> 72 | null 73 | else -> throw IllegalArgumentException() 74 | } 75 | } 76 | } 77 | 78 | return OsLocationProvider(name, properties, fudger, permission) 79 | } 80 | 81 | private fun createLocationFudger(): LocationFudger { 82 | /** @see com.android.server.location.injector.SystemSettingsHelper */ 83 | val DEFAULT_COARSE_LOCATION_ACCURACY_M = 2000.0f 84 | return LocationFudger(DEFAULT_COARSE_LOCATION_ACCURACY_M) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 23 | 24 | 25 | 26 | 30 | 33 | 36 | 37 | 40 | 44 | 45 | 51 | 57 | 58 | 65 | 66 | 70 | 71 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/location/OsLocationListener.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.location 2 | 3 | import android.location.Location 4 | import android.location.LocationListener 5 | import android.location.LocationManager 6 | import android.os.SystemClock 7 | import app.grapheneos.gmscompat.Const 8 | import app.grapheneos.gmscompat.logd 9 | import com.google.android.gms.location.LocationAvailability 10 | import com.google.android.gms.location.LocationRequest 11 | import java.util.Collections 12 | import java.util.concurrent.CountDownLatch 13 | import kotlin.math.max 14 | import kotlin.math.min 15 | 16 | fun LocationRequest.toOsLocationRequest(): android.location.LocationRequest { 17 | val interval = 18 | if (priority == LocationRequest.PRIORITY_NO_POWER) { 19 | android.location.LocationRequest.PASSIVE_INTERVAL 20 | } else { 21 | interval 22 | } 23 | val b = android.location.LocationRequest.Builder(interval) 24 | val quality = gmsPriorityToOsQuality(priority) 25 | b.setQuality(quality) 26 | b.setMinUpdateIntervalMillis(minUpdateIntervalMillis) 27 | b.setMaxUpdates(maxUpdates) 28 | b.setMinUpdateDistanceMeters(minUpdateDistanceMeters) 29 | b.setMaxUpdateDelayMillis(maxUpdateDelayMillis) 30 | 31 | if (expirationTime != Long.MAX_VALUE) { 32 | durationMillis = min(max(1L, expirationTime - SystemClock.elapsedRealtime()), durationMillis) 33 | } 34 | b.setDurationMillis(durationMillis) 35 | 36 | return b.build() 37 | } 38 | 39 | fun gmsPriorityToOsQuality(priority: Int): Int = 40 | when (priority) { 41 | LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY -> 42 | android.location.LocationRequest.QUALITY_BALANCED_POWER_ACCURACY 43 | LocationRequest.PRIORITY_HIGH_ACCURACY -> 44 | android.location.LocationRequest.QUALITY_HIGH_ACCURACY 45 | LocationRequest.PRIORITY_LOW_POWER -> 46 | android.location.LocationRequest.QUALITY_LOW_POWER 47 | LocationRequest.PRIORITY_NO_POWER -> 48 | android.location.LocationRequest.QUALITY_LOW_POWER 49 | else -> 50 | throw IllegalArgumentException() 51 | } 52 | 53 | class OsLocationListener(val client: Client, val provider: OsLocationProvider, 54 | val request: android.location.LocationRequest, 55 | val forwarder: GLocationForwarder 56 | ) : LocationListener { 57 | override fun onLocationChanged(location: Location) { 58 | onLocationChanged(Collections.singletonList(location)) 59 | } 60 | 61 | override fun onLocationChanged(locations: List) { 62 | locations.forEach { 63 | // mimic GMS location service 64 | it.provider = LocationManager.FUSED_PROVIDER 65 | } 66 | 67 | val locationsToForward = locations.map { provider.maybeFudge(it) } 68 | 69 | if (false) { 70 | // simulate movement 71 | locationsToForward.forEach { 72 | val off = (SystemClock.uptimeMillis() % 10_000) / 1_000_000.0 73 | it.latitude += off 74 | it.longitude += off 75 | } 76 | } 77 | var unregister = true 78 | try { 79 | forwarder.forwardLocations(client.ctx, locationsToForward) 80 | unregister = false 81 | } catch (e: Exception) { 82 | logd{e} 83 | } 84 | if (unregister) { 85 | forwarder.unregister() 86 | } 87 | } 88 | 89 | private fun onLocationAvailabilityChanged(available: Boolean) { 90 | try { 91 | forwarder.onLocationAvailabilityChanged(client.ctx, LocationAvailability.get(available)) 92 | } catch (e: Exception) { 93 | logd{e} 94 | forwarder.unregister() 95 | } 96 | } 97 | 98 | override fun onProviderEnabled(provider: String) { 99 | logd{provider} 100 | check(provider == this.provider.name) 101 | onLocationAvailabilityChanged(true) 102 | } 103 | 104 | override fun onProviderDisabled(provider: String) { 105 | logd{provider} 106 | check(provider == this.provider.name) 107 | onLocationAvailabilityChanged(false) 108 | } 109 | 110 | private var flushLatch: CountDownLatch? = null 111 | 112 | fun flush() { 113 | val l = CountDownLatch(1) 114 | synchronized(this) { 115 | flushLatch = l 116 | try { 117 | client.locationManager.requestFlush(provider.name, this, 0) 118 | } catch (e: IllegalStateException) { 119 | // may get thrown if other thread unregisters this listener 120 | return 121 | } 122 | l.await() 123 | flushLatch = null 124 | } 125 | } 126 | 127 | override fun onFlushComplete(requestCode: Int) { 128 | flushLatch!!.countDown() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/PersistentFgService.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat; 2 | 3 | import android.app.Notification; 4 | import android.app.PendingIntent; 5 | import android.app.Service; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.os.IBinder; 9 | import android.os.ParcelUuid; 10 | import android.util.ArrayMap; 11 | import android.util.Log; 12 | 13 | import java.util.UUID; 14 | import java.util.concurrent.CountDownLatch; 15 | 16 | // The purpose of this service is to raise GmsCompat app to foreground service level while GMS 17 | // components are running. This allows to host long-living objects in GmsCompat app process and to 18 | // temporarily raise GMS components and/or their clients to foreground level when that's required for 19 | // starting a service, receiving a FCM notification, etc. 20 | public class PersistentFgService extends Service { 21 | private static final String TAG = PersistentFgService.class.getSimpleName(); 22 | 23 | public static CountDownLatch requestStart() { 24 | return command(CMD_START); 25 | } 26 | 27 | public static CountDownLatch release() { 28 | return command(CMD_MAYBE_STOP); 29 | } 30 | 31 | // access only from the main thread 32 | private static final ArrayMap pendingCommands = new ArrayMap<>(7); 33 | 34 | private static CountDownLatch command(String cmd) { 35 | // otherwise CountDownLatch will deadlock 36 | UtilsKt.notMainThread(); 37 | 38 | Context ctx = App.ctx(); 39 | 40 | var latch = new CountDownLatch(1); 41 | 42 | // onStartCommand() is called on the main thread, see pendingCommands operations for why 43 | // this is important 44 | ctx.getMainThreadHandler().post(() -> { 45 | var i = new Intent(ctx, PersistentFgService.class); 46 | i.setAction(cmd); 47 | 48 | ParcelUuid commandId = new ParcelUuid(UUID.randomUUID()); 49 | i.putExtra(EXTRA_COMMAND_ID, commandId); 50 | 51 | if (pendingCommands.put(commandId, latch) != null) { 52 | throw new IllegalStateException("duplicate commandId"); 53 | } 54 | Log.d(TAG, "command " + cmd + ", id " + commandId); 55 | ctx.startForegroundService(i); 56 | }); 57 | 58 | return latch; 59 | } 60 | 61 | private static final String CMD_START = "start"; 62 | private static final String CMD_MAYBE_STOP = "maybe_stop"; 63 | private static final String EXTRA_COMMAND_ID = "cmd_id"; 64 | 65 | private int referenceCount; 66 | 67 | @Override 68 | public int onStartCommand(Intent intent, int flags, int startId) { 69 | String cmd = intent.getAction(); 70 | ParcelUuid commandId = intent.getParcelableExtra(EXTRA_COMMAND_ID, ParcelUuid.class); 71 | int refCount = referenceCount; 72 | Log.d(TAG, "onStartCommand, cmd " + cmd + ", refCount " + refCount + ", id " + commandId); 73 | 74 | CountDownLatch latch = pendingCommands.remove(commandId); 75 | if (latch == null) { 76 | if (refCount != 0) { 77 | throw new IllegalStateException("invalid ref count: " + refCount + ", commandId " + commandId); 78 | } 79 | Log.d(TAG, "ignoring command from previous process instance"); 80 | // OS requires that startForeground() is called after startForegroundService() 81 | startForeground(Notification.FOREGROUND_SERVICE_DEFERRED); 82 | stopSelf(); 83 | return START_NOT_STICKY; 84 | } 85 | 86 | switch (cmd) { 87 | case CMD_START -> { 88 | ++refCount; 89 | if (refCount == 1) { 90 | startForeground(Notification.FOREGROUND_SERVICE_IMMEDIATE); 91 | } 92 | } 93 | case CMD_MAYBE_STOP -> { 94 | --refCount; 95 | if (refCount == 0) { 96 | stopSelf(); 97 | } 98 | } 99 | default -> throw new IllegalStateException(cmd); 100 | } 101 | if (refCount < 0) { 102 | throw new IllegalStateException("invalid ref count: " + referenceCount); 103 | } 104 | referenceCount = refCount; 105 | latch.countDown(); 106 | 107 | return START_NOT_STICKY; 108 | } 109 | 110 | private void startForeground(int notifBehavior) { 111 | Notification.Builder nb = Notifications.builder(Notifications.CH_PERSISTENT_FG_SERVICE); 112 | nb.setSmallIcon(android.R.drawable.ic_dialog_dialer); 113 | nb.setContentTitle(getText(R.string.persistent_fg_service_notif)); 114 | nb.setContentIntent(PendingIntent.getActivity(this, 0, 115 | new Intent(this, MainActivity.class), PendingIntent.FLAG_IMMUTABLE)); 116 | nb.setForegroundServiceBehavior(notifBehavior); 117 | nb.setGroup(Notifications.CH_PERSISTENT_FG_SERVICE); 118 | startForeground(Notifications.ID_PERSISTENT_FG_SERVICE, nb.build()); 119 | } 120 | 121 | @Override 122 | public void onCreate() { 123 | Log.d(TAG, "onCreate"); 124 | } 125 | 126 | @Override 127 | public void onDestroy() { 128 | Log.d(TAG, "onDestroy"); 129 | } 130 | 131 | @Override 132 | public IBinder onBind(Intent intent) { 133 | throw new IllegalStateException(intent.toString()); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/src/app/grapheneos/gmscompat/lib/util/BaseIContentProvider.java: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat.lib.util; 2 | 3 | import android.annotation.NonNull; 4 | import android.annotation.Nullable; 5 | import android.content.AttributionSource; 6 | import android.content.ContentProviderOperation; 7 | import android.content.ContentProviderResult; 8 | import android.content.ContentValues; 9 | import android.content.IContentProvider; 10 | import android.content.OperationApplicationException; 11 | import android.content.res.AssetFileDescriptor; 12 | import android.database.Cursor; 13 | import android.net.Uri; 14 | import android.os.Binder; 15 | import android.os.Bundle; 16 | import android.os.IBinder; 17 | import android.os.ICancellationSignal; 18 | import android.os.ParcelFileDescriptor; 19 | import android.os.RemoteCallback; 20 | import android.os.RemoteException; 21 | 22 | import java.io.FileNotFoundException; 23 | import java.util.ArrayList; 24 | 25 | public class BaseIContentProvider implements IContentProvider { 26 | 27 | @Override 28 | public Cursor query(@NonNull AttributionSource attributionSource, Uri url, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable ICancellationSignal cancellationSignal) throws RemoteException { 29 | return null; 30 | } 31 | 32 | @Override 33 | public String getType(@NonNull AttributionSource attributionSource, Uri url) throws RemoteException { 34 | return ""; 35 | } 36 | 37 | @Override 38 | public void getTypeAsync(@NonNull AttributionSource attributionSource, Uri url, RemoteCallback callback) throws RemoteException { 39 | 40 | } 41 | 42 | @Override 43 | public void getTypeAnonymousAsync(Uri uri, RemoteCallback callback) throws RemoteException { 44 | 45 | } 46 | 47 | @Override 48 | public Uri insert(@NonNull AttributionSource attributionSource, Uri url, ContentValues initialValues, Bundle extras) throws RemoteException { 49 | return null; 50 | } 51 | 52 | @Override 53 | public int bulkInsert(@NonNull AttributionSource attributionSource, Uri url, ContentValues[] initialValues) throws RemoteException { 54 | return 0; 55 | } 56 | 57 | @Override 58 | public int delete(@NonNull AttributionSource attributionSource, Uri url, Bundle extras) throws RemoteException { 59 | return 0; 60 | } 61 | 62 | @Override 63 | public int update(@NonNull AttributionSource attributionSource, Uri url, ContentValues values, Bundle extras) throws RemoteException { 64 | return 0; 65 | } 66 | 67 | @Override 68 | public ParcelFileDescriptor openFile(@NonNull AttributionSource attributionSource, Uri url, String mode, ICancellationSignal signal) throws RemoteException, FileNotFoundException { 69 | return null; 70 | } 71 | 72 | @Override 73 | public AssetFileDescriptor openAssetFile(@NonNull AttributionSource attributionSource, Uri url, String mode, ICancellationSignal signal) throws RemoteException, FileNotFoundException { 74 | return null; 75 | } 76 | 77 | @Override 78 | public ContentProviderResult[] applyBatch(@NonNull AttributionSource attributionSource, String authority, ArrayList operations) throws RemoteException, OperationApplicationException { 79 | return new ContentProviderResult[0]; 80 | } 81 | 82 | @Override 83 | public Bundle call(@NonNull AttributionSource attributionSource, String authority, String method, @Nullable String arg, @Nullable Bundle extras) throws RemoteException { 84 | return null; 85 | } 86 | 87 | @Override 88 | public int checkUriPermission(@NonNull AttributionSource attributionSource, Uri uri, int uid, int modeFlags) throws RemoteException { 89 | return 0; 90 | } 91 | 92 | @Override 93 | public ICancellationSignal createCancellationSignal() throws RemoteException { 94 | return null; 95 | } 96 | 97 | @Override 98 | public Uri canonicalize(@NonNull AttributionSource attributionSource, Uri uri) throws RemoteException { 99 | return null; 100 | } 101 | 102 | @Override 103 | public void canonicalizeAsync(@NonNull AttributionSource attributionSource, Uri uri, RemoteCallback callback) throws RemoteException { 104 | 105 | } 106 | 107 | @Override 108 | public Uri uncanonicalize(@NonNull AttributionSource attributionSource, Uri uri) throws RemoteException { 109 | return null; 110 | } 111 | 112 | @Override 113 | public void uncanonicalizeAsync(@NonNull AttributionSource attributionSource, Uri uri, RemoteCallback callback) throws RemoteException { 114 | 115 | } 116 | 117 | @Override 118 | public boolean refresh(@NonNull AttributionSource attributionSource, Uri url, @Nullable Bundle extras, ICancellationSignal cancellationSignal) throws RemoteException { 119 | return false; 120 | } 121 | 122 | @Override 123 | public String[] getStreamTypes(AttributionSource attributionSource, Uri url, String mimeTypeFilter) throws RemoteException { 124 | return new String[0]; 125 | } 126 | 127 | @Override 128 | public AssetFileDescriptor openTypedAssetFile(@NonNull AttributionSource attributionSource, Uri url, String mimeType, Bundle opts, ICancellationSignal signal) throws RemoteException, FileNotFoundException { 129 | return null; 130 | } 131 | 132 | private final Binder binderStub = new Binder(); 133 | 134 | @Override 135 | public IBinder asBinder() { 136 | return binderStub; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/BinderClientOfGmsCore2Gca.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.app.Notification 4 | import android.app.PendingIntent 5 | import android.app.compat.gms.GmsCompat 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.pm.GosPackageState 9 | import android.content.pm.GosPackageStateFlag 10 | import android.credentials.CredentialManager 11 | import android.ext.PackageId 12 | import android.ext.SettingsIntents 13 | import android.ext.settings.app.AswBlockPlayIntegrityApi 14 | import android.net.Uri 15 | import android.os.Binder 16 | import android.os.BinderDef 17 | import android.provider.Settings 18 | import android.util.Log 19 | import androidx.core.content.getSystemService 20 | import com.android.internal.gmscompat.IClientOfGmsCore2Gca 21 | import com.android.internal.gmscompat.dynamite.server.IFileProxyService 22 | 23 | object BinderClientOfGmsCore2Gca : IClientOfGmsCore2Gca.Stub() { 24 | 25 | override fun maybeGetBinderDef(callerPkg: String, processState: Int, ifaceName: String): BinderDef? { 26 | return BinderDefs.maybeGetBinderDef(callerPkg, processState, ifaceName, false) 27 | } 28 | 29 | override fun getDynamiteFileProxyService(): IFileProxyService = BinderGms2Gca.dynamiteFileProxyService!! 30 | 31 | override fun showMissingAppNotification(pkgName: String) { 32 | val prompt = when (pkgName) { 33 | "com.google.android.tts" -> R.string.missing_speech_services 34 | else -> throw IllegalArgumentException(pkgName) 35 | } 36 | 37 | Notifications.handleMissingApp(Notifications.CH_MISSING_APP, App.ctx().getText(prompt), pkgName) 38 | } 39 | 40 | override fun showPlayIntegrityNotification(pkgName: String, isBlocked: Boolean) { 41 | val ctx = App.ctx() 42 | if (ctx.packageManager.getPackageUid(pkgName, 0) != Binder.getCallingUid()) { 43 | // this method should be called by app itself 44 | throw SecurityException() 45 | } 46 | 47 | val TAG = "showPlayIntegrityNotification" 48 | Log.d(TAG, "caller: $pkgName") 49 | 50 | val setting = AswBlockPlayIntegrityApi.I 51 | val gosPs = GosPackageState.get(pkgName, ctx.userId) 52 | if (!setting.isNotificationEnabled(gosPs)) { 53 | // there's a client-side isNotificationEnabled() check before the call 54 | Log.e(TAG, "notification is disabled") 55 | return 56 | } 57 | 58 | Notifications.builder(Notifications.CH_MANAGE_PLAY_INTEGRITY_API).run { 59 | setSmallIcon(R.drawable.ic_info) 60 | setContentTitle(R.string.notif_app_used_play_integrity_api_title) 61 | val text = if (isBlocked) R.string.notif_app_play_integrity_api_blocked 62 | else R.string.notif_app_used_play_integrity_api 63 | setContentText(ctx.getString(text, getApplicationLabel(ctx, pkgName))) 64 | run { 65 | val intent = SettingsIntents.getAppIntent(ctx, SettingsIntents.APP_MANAGE_PLAY_INTEGRITY_API, pkgName) 66 | val pendingIntent = PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) 67 | addAction(Notification.Action.Builder(null, 68 | ctx.getText(R.string.notif_app_used_play_integrity_api_action_more_info), 69 | pendingIntent).build()) 70 | } 71 | run { 72 | val pendingIntent = PendingActionReceiver.makeWriteGosPackageStateAndCancelNotif( 73 | ctx, pkgName, 74 | GosPackageStateFlag.SUPPRESS_PLAY_INTEGRITY_API_NOTIF, true, 75 | Notifications.ID_MANAGE_PLAY_INTEGRITY_API) 76 | addAction(Notification.Action.Builder(null, 77 | ctx.getText(R.string.dont_show_again), pendingIntent).build()) 78 | } 79 | setShowWhen(true) 80 | show(Notifications.ID_MANAGE_PLAY_INTEGRITY_API) 81 | } 82 | } 83 | 84 | override fun onGoogleIdCredentialOptionInit() { 85 | val ctx = App.ctx() 86 | if (!GmsCompat.isEnabledFor(PackageId.GMS_CORE_NAME, ctx.userId)) { 87 | return 88 | } 89 | 90 | if (isGoogleIdCredentialProviderEnabled(ctx)) { 91 | return 92 | } 93 | 94 | Notifications.builder(Notifications.CH_SIGN_IN_WITH_GOOGLE).run { 95 | val intent = Intent(Settings.ACTION_CREDENTIAL_PROVIDER, Uri.parse("package:" + PackageId.GMS_CORE_NAME)) 96 | setContentIntent(PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)) 97 | 98 | setSmallIcon(R.drawable.ic_configuration_required) 99 | setContentTitle(ctx.getText(R.string.sign_in_with_google_notif_title)) 100 | setContentText(ctx.getText(R.string.sign_in_with_google_notif_text)) 101 | setAutoCancel(true) 102 | show(Notifications.ID_ENABLE_GOOGLE_CREDENTIAL_PROVIDER) 103 | } 104 | } 105 | 106 | private fun isGoogleIdCredentialProviderEnabled(ctx: Context): Boolean { 107 | val credentialM = ctx.getSystemService()!! 108 | // isEnabledCredentialProviderService only allows to query app's own services 109 | val providers = credentialM.getCredentialProviderServices(ctx.userId, 110 | CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY) 111 | 112 | return providers.any { 113 | it.isEnabled 114 | && it.componentName.packageName == PackageId.GMS_CORE_NAME 115 | && it.hasCapability("com.google.android.libraries.identity.googleid.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL") 116 | } 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/BinderDefs.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.Manifest 4 | import android.app.ActivityManager 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.ext.PackageId 8 | import android.os.BinderDef 9 | import android.util.ArrayMap 10 | import android.util.ArraySet 11 | import android.util.Log 12 | import androidx.core.content.edit 13 | import app.grapheneos.gmscompat.App.MainProcessPrefs 14 | import app.grapheneos.gmscompat.location.GLocationService 15 | import com.android.internal.gmscompat.GmsInfo 16 | import com.android.internal.gmscompat.GmcBinderDefs.BinderDefStateListener 17 | 18 | enum class BinderDefGroup(val services: Array) { 19 | LOCATION(arrayOf(GLocationService.BinderDef())) 20 | } 21 | 22 | object BinderDefs { 23 | 24 | private val ifaceNameToGroup = ArrayMap().also { map -> 25 | BinderDefGroup.values().forEach { group -> 26 | group.services.forEach { 27 | map.put(it.ifaceName, group) 28 | } 29 | } 30 | } 31 | 32 | private val ifaceNameToBinderDef = ArrayMap().also { map -> 33 | BinderDefGroup.values().forEach { group -> 34 | group.services.forEach { 35 | map.put(it.ifaceName, it) 36 | } 37 | } 38 | } 39 | 40 | fun getFromIfaceNameIfEnabled(callerPkg: String, ifaceName: String): BinderDef? { 41 | if (isEnabled(callerPkg, ifaceName)) { 42 | return getFromIfaceName(callerPkg, ifaceName) 43 | } 44 | return null 45 | } 46 | 47 | fun getFromIfaceName(callerPkg: String, ifaceName: String): BinderDef { 48 | return ifaceNameToBinderDef.get(ifaceName)!!.get(App.ctx(), callerPkg) 49 | } 50 | 51 | private fun getGroupFromIfaceName(ifaceName: String): BinderDefGroup { 52 | return ifaceNameToGroup.get(ifaceName)!! 53 | } 54 | 55 | private fun isEnabled(callerPkg: String, ifaceName: String): Boolean { 56 | return isEnabled(getGroupFromIfaceName(ifaceName), callerPkg) 57 | } 58 | 59 | fun isEnabled(group: BinderDefGroup, callerPkg: String? = null): Boolean { 60 | val prefs = App.preferences() 61 | return when (group) { 62 | BinderDefGroup.LOCATION -> { 63 | if (callerPkg == PackageId.GMS_CORE_NAME || callerPkg == PackageId.ANDROID_AUTO_NAME) { 64 | if (App.ctx().packageManager.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION, callerPkg) != PackageManager.PERMISSION_GRANTED) { 65 | // GmsCore and Android Auto crash in some cases when the GmsCore location 66 | // service is used without holding any location permission. 67 | // 68 | // Usage of the GmsCompat location service reimplementation avoid this issue. 69 | return true 70 | } 71 | } 72 | val k = MainProcessPrefs.LOCATION_REQUEST_REDIRECTION_ENABLED 73 | // getLong is used for compatibility with historical setting key 74 | val v = prefs.getLong(k, -1L) 75 | if (v == -1L) { 76 | val isEnabledByDefault = !gmsCoreHasPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) 77 | prefs.edit { 78 | putLong(k, if (isEnabledByDefault) 1 else 0) 79 | } 80 | return isEnabledByDefault 81 | } 82 | v == 1L 83 | } 84 | } 85 | } 86 | 87 | fun setEnabled(group: BinderDefGroup, enabled: Boolean) { 88 | App.preferences().edit { 89 | when (group) { 90 | BinderDefGroup.LOCATION -> 91 | // putLong is used for compatibility with historical setting key 92 | putLong(MainProcessPrefs.LOCATION_REQUEST_REDIRECTION_ENABLED, if (enabled) 1 else 0) 93 | } 94 | } 95 | 96 | val changedIfaceNames: Array = group.services.map { it.ifaceName }.toTypedArray() 97 | 98 | val intent = Intent(BinderDefStateListener.INTENT_ACTION).apply { 99 | putExtra(BinderDefStateListener.KEY_CHANGED_IFACE_NAMES, changedIfaceNames) 100 | } 101 | App.ctx().sendBroadcast(intent) 102 | } 103 | 104 | val binderDefs: Set = getIfaceNames(BinderDefGroup.LOCATION) 105 | val notableIfaceNames: Map = NotableInterface.values().associateBy { it.ifaceName } 106 | 107 | fun maybeGetBinderDef(callerPkg: String, processState: Int, ifaceName: String, isFromGms2Gca: Boolean): BinderDef? { 108 | // Note that the callerPkg value is not verified at this point, verification is delayed 109 | // until the time of first use to reduce perf impact 110 | 111 | val TAG = "maybeGetBinderDef" 112 | if (Log.isLoggable(TAG, Log.VERBOSE)) { 113 | Log.v(TAG, "caller $callerPkg processState ${ActivityManager.procStateToString(processState)} $ifaceName") 114 | } 115 | 116 | val notableIface = notableIfaceNames.get(ifaceName) 117 | if (notableIface != null) { 118 | verifyCallerPkg(callerPkg) 119 | 120 | App.bgExecutor().execute { 121 | notableIface.onAcquiredByClient(callerPkg, processState) 122 | } 123 | } 124 | 125 | if (binderDefs.contains(ifaceName)) { 126 | verifyCallerPkg(callerPkg) 127 | 128 | return getFromIfaceNameIfEnabled(callerPkg, ifaceName) 129 | } 130 | 131 | return null 132 | } 133 | 134 | fun getIfaceNames(vararg groups: BinderDefGroup): Set { 135 | val set = ArraySet() 136 | groups.forEach { 137 | it.services.forEach { 138 | set.add(it.ifaceName) 139 | } 140 | } 141 | return set 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/PrivSettings.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.database.IContentObserver 6 | import android.net.Uri 7 | import android.os.DeadObjectException 8 | import android.os.IBinder 9 | import android.os.Process 10 | import android.os.RemoteException 11 | import android.provider.Settings 12 | import android.util.ArraySet 13 | import android.util.Pair 14 | import java.util.* 15 | 16 | typealias NsKey = Pair 17 | 18 | // A partial reimplementation of Settings.{Global,Secure} and DeviceConfig. 19 | class PrivSettings : IBinder.DeathRecipient { 20 | private val storageMap = HashMap() 21 | private val observerMap = HashMap>() 22 | private val reverseObserverMap = HashMap>() 23 | 24 | private val DBG = false 25 | 26 | private fun storage(namespace: String): SharedPreferences { 27 | return synchronized(storageMap) { 28 | storageMap.getOrPut(namespace) { 29 | App.deviceProtectedStorageContext() 30 | .getSharedPreferences(namespace, Context.MODE_PRIVATE) 31 | } 32 | } 33 | } 34 | 35 | fun getString(ns: String, key: String): String? { 36 | val storage = storage(ns) 37 | val value = synchronized(storage) { 38 | storage.getString(key, null) 39 | } 40 | if (DBG) logd{"$ns $key $value"} 41 | return value 42 | } 43 | 44 | fun putString(ns: String, key: String, value: String?): Boolean { 45 | if (DBG) logd{"$ns $key $value"} 46 | 47 | val storage = storage(ns) 48 | val res = synchronized(storage) { 49 | storage.edit().putString(key, value).commit() 50 | } 51 | 52 | notifyObservers(ns, key) 53 | return res 54 | } 55 | 56 | fun getStrings(ns: String, keys: Array, 57 | foundKeys: MutableList, foundValues: MutableList) { 58 | if (DBG) logd{"$ns keys " + Arrays.toString(keys)} 59 | 60 | val storage = storage(ns) 61 | synchronized(storage) { 62 | keys.forEach { key -> 63 | storage.getString(key, null)?.let { value -> 64 | if (DBG) logd{"$ns $key $value"} 65 | foundKeys.add(key) 66 | foundValues.add(value) 67 | } 68 | } 69 | } 70 | } 71 | 72 | fun putStrings(ns: String, keys: Array, values: Array): Boolean { 73 | if (DBG) logd{"$ns keys " + Arrays.toString(keys) + " values " + Arrays.toString(values)} 74 | 75 | val storage = storage(ns) 76 | 77 | val res = synchronized(storage) { 78 | val ed = storage.edit() 79 | for (i in 0 until keys.size) { 80 | ed.putString(keys[i], values[i]) 81 | } 82 | ed.commit() 83 | } 84 | 85 | keys.forEach { 86 | notifyObservers(ns, it) 87 | } 88 | 89 | return res 90 | } 91 | 92 | fun addObserver(ns: String, key: String, observer: IContentObserver) { 93 | if (DBG) logd{"$ns $key ${observer.asBinder()}"} 94 | 95 | val nsKey = NsKey.create(ns, key) 96 | val binder = observer.asBinder() 97 | 98 | synchronized(observerMap) { 99 | if (reverseObserverMap[binder] == null) { 100 | try { 101 | binder.linkToDeath(this, 0) 102 | } catch (e: RemoteException) { 103 | logd{"observer already died " + e} 104 | return 105 | } 106 | } 107 | 108 | observerMap.getOrPut(nsKey) { 109 | ArraySet() 110 | }.add(observer) 111 | 112 | reverseObserverMap.getOrPut(binder) { 113 | ArraySet() 114 | }.add(nsKey) 115 | } 116 | } 117 | 118 | override fun binderDied() { 119 | // should never be reached if binderDied(IBinder) is overriden 120 | throw IllegalStateException() 121 | } 122 | 123 | override fun binderDied(who: IBinder) { 124 | removeObserver(who) 125 | } 126 | 127 | fun removeObserver(observer: IContentObserver) { 128 | removeObserver(observer.asBinder()) 129 | } 130 | 131 | private fun removeObserver(observer: IBinder) { 132 | if (DBG) logd{"$observer" + Throwable()} 133 | 134 | synchronized(observerMap) { 135 | observer.unlinkToDeath(this, 0) 136 | 137 | val nsKeys = reverseObserverMap[observer] ?: return 138 | reverseObserverMap.remove(observer) 139 | 140 | nsKeys.forEach { nsKey -> 141 | val set = observerMap[nsKey] ?: return@forEach 142 | 143 | var found = false 144 | 145 | for (i in 0 until set.size) { 146 | if (set.valueAt(i).asBinder() === observer) { 147 | set.removeAt(i) 148 | found = true 149 | break 150 | } 151 | } 152 | 153 | if (DBG) check(found) 154 | 155 | if (set.size == 0) { 156 | observerMap.remove(nsKey) 157 | } 158 | } 159 | } 160 | } 161 | 162 | private fun notifyObservers(ns: String, key: String, flags: Int = 0) { 163 | val nsKey = NsKey(ns, key) 164 | val namespaceUri = Uri.parse("content://${Settings.AUTHORITY}/$ns") 165 | val uri = Settings.NameValueTable.getUriFor(namespaceUri, key) 166 | val uriArray = arrayOf(uri) 167 | val userId = Process.myUserHandle().identifier 168 | 169 | synchronized(observerMap) { 170 | val observers = observerMap[nsKey]?.toTypedArray() ?: return 171 | 172 | if (DBG) logd{"$ns $key to ${observers.size} observers"} 173 | 174 | observers.forEach { 175 | try { 176 | it.onChangeEtc(false, uriArray, flags, userId) 177 | } catch (doe: DeadObjectException) { 178 | logd{"listener died $doe"} 179 | } 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/Utils.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.app.AppOpsManager 4 | import android.app.PendingIntent 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.ApplicationInfo 8 | import android.content.pm.PackageManager 9 | import android.net.Uri 10 | import android.os.Bundle 11 | import android.provider.Settings 12 | import android.util.Log 13 | import app.grapheneos.gmscompat.Const.ENABLE_LOGGING 14 | import com.android.internal.gmscompat.GmsInfo 15 | import java.lang.reflect.Modifier 16 | import java.util.UUID 17 | 18 | fun mainThread() { 19 | check(Thread.currentThread() === App.mainThread()) 20 | } 21 | 22 | fun notMainThread() { 23 | check(Thread.currentThread() !== App.mainThread()) 24 | } 25 | 26 | fun logd() { 27 | if (!ENABLE_LOGGING) { 28 | return 29 | } 30 | logInternal("<>", Log.DEBUG, 4) 31 | } 32 | 33 | fun logds(msg: String) { 34 | if (!ENABLE_LOGGING) { 35 | return 36 | } 37 | logInternal(msg, Log.DEBUG, 4) 38 | } 39 | 40 | inline fun logd(msg: () -> Any?) { 41 | if (!ENABLE_LOGGING) { 42 | return 43 | } 44 | logInternal(msg(), Log.DEBUG, 3) 45 | } 46 | 47 | inline fun logw(msg: () -> Any?) { 48 | if (!ENABLE_LOGGING) { 49 | return 50 | } 51 | logInternal(msg(), Log.WARN, 3) 52 | } 53 | 54 | inline fun log(msg: () -> Any?, level: Int) { 55 | if (!ENABLE_LOGGING) { 56 | return 57 | } 58 | logInternal(msg(), level, 3) 59 | } 60 | 61 | fun logInternal(o: Any?, level: Int, depth: Int) { 62 | if (!ENABLE_LOGGING) { 63 | return 64 | } 65 | val e = Thread.currentThread().stackTrace[depth] 66 | val sb = StringBuilder(100) 67 | sb.append(e.getMethodName()) 68 | sb.append(" (") 69 | sb.append(e.getFileName()) 70 | sb.append(':') 71 | sb.append(e.getLineNumber()) 72 | sb.append(')') 73 | Log.println(level, sb.toString(), objectToString(o)) 74 | } 75 | 76 | fun objectToString(o: Any?): String { 77 | if (o == null || o is String || o is Number || o is Boolean || o is Char) { 78 | return o.toString() 79 | } 80 | val b = StringBuilder(100) 81 | b.append(o.javaClass.name) 82 | b.append(" [ ") 83 | o.javaClass.fields.forEach { 84 | if (!Modifier.isStatic(it.modifiers)) { 85 | b.append(it.name) 86 | b.append(": ") 87 | b.append(it.get(o)) 88 | // b.append(objectToString(it.get(o))) 89 | b.append(", ") 90 | } 91 | } 92 | b.append("]") 93 | return b.toString() 94 | } 95 | 96 | fun opModeToString(mode: Int): String = 97 | when (mode) { 98 | AppOpsManager.MODE_ALLOWED -> "MODE_ALLOWED" 99 | AppOpsManager.MODE_IGNORED -> "MODE_IGNORED" 100 | AppOpsManager.MODE_ERRORED -> "MODE_ERRORED" 101 | AppOpsManager.MODE_DEFAULT -> "MODE_DEFAULT" 102 | AppOpsManager.MODE_FOREGROUND -> "MODE_FOREGROUND" 103 | else -> error(mode) 104 | } 105 | 106 | fun gmsCoreHasPermission(perm: String): Boolean { 107 | return appHasPermission(GmsInfo.PACKAGE_GMS_CORE, perm) 108 | } 109 | 110 | fun playStoreHasPermission(perm: String): Boolean { 111 | return appHasPermission(GmsInfo.PACKAGE_PLAY_STORE, perm) 112 | } 113 | 114 | fun appHasPermission(pkg: String, perm: String): Boolean { 115 | return App.ctx().packageManager.checkPermission(perm, pkg) == PackageManager.PERMISSION_GRANTED 116 | } 117 | 118 | fun isPkgInstalled(pkg: String): Boolean { 119 | try { 120 | App.ctx().packageManager.getPackageInfo(pkg, PackageManager.PackageInfoFlags.of(0)) 121 | return true 122 | } catch (e: PackageManager.NameNotFoundException) { 123 | return false 124 | } 125 | } 126 | 127 | fun PackageManager.getAppInfoOrNull(pkgName: String, flags: Long = 0L): ApplicationInfo? { 128 | return try { 129 | getApplicationInfo(pkgName, PackageManager.ApplicationInfoFlags.of(flags)) 130 | } catch (e: PackageManager.NameNotFoundException) { 131 | null 132 | } 133 | } 134 | 135 | fun checkPackageId(pkg: String, id: Int): Boolean { 136 | try { 137 | return App.ctx().packageManager.getApplicationInfo(pkg, 0).ext().packageId == id 138 | } catch (e: PackageManager.NameNotFoundException) { 139 | return false 140 | } 141 | } 142 | 143 | fun freshActivity(intent: Intent): Intent { 144 | // needed to ensure consistent behavior, 145 | // otherwise existing instance that is in unknown state could be shown 146 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 147 | return intent 148 | } 149 | 150 | fun getApplicationLabel(ctx: Context, pkg: String): CharSequence { 151 | val pm = ctx.packageManager 152 | return pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)) 153 | } 154 | 155 | fun verifyCallerPkg(providedPkgName: String) { 156 | val ctx = App.ctx() 157 | val pm = ctx.packageManager 158 | check(pm.getApplicationInfo(providedPkgName, 0).uid == android.os.Binder.getCallingUid()) 159 | } 160 | 161 | const val APP_INFO_ITEM_PERMISSIONS = "permission_settings" 162 | 163 | fun appSettingsIntent(pkg: String, item: String? = null): Intent { 164 | val uri = Uri.fromParts("package", pkg, null) 165 | val i = freshActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)) 166 | if (item != null) { 167 | val args = Bundle() 168 | // :settings constants aren't exposed by Settings as part of its API, but are used in 169 | // multiple places in the OS 170 | args.putString(":settings:fragment_args_key", item) 171 | i.putExtra(":settings:show_fragment_args", args) 172 | } 173 | return i 174 | } 175 | 176 | fun notificationSettingsIntent(pkg: String): Intent { 177 | return Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { 178 | putExtra(Settings.EXTRA_APP_PACKAGE, pkg) 179 | } 180 | } 181 | 182 | fun gmsCoreSettings() = appSettingsIntent(GmsInfo.PACKAGE_GMS_CORE) 183 | fun playStoreSettings() = appSettingsIntent(GmsInfo.PACKAGE_PLAY_STORE) 184 | 185 | fun appSettingsPendingIntent(pkg: String, item: String? = null) = 186 | activityPendingIntent(appSettingsIntent(pkg, item)) 187 | 188 | fun activityPendingIntent(i: Intent, flags: Int = PendingIntent.FLAG_IMMUTABLE): PendingIntent { 189 | i.setIdentifier(UUID.randomUUID().toString()) 190 | i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 191 | return PendingIntent.getActivity(App.ctx(), 0, i, flags) 192 | } 193 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GmsCompat 3 | Sandboxed Google Play 4 | Reroute location requests to the OS 5 | Open app info 6 | Open settings 7 | Sandboxed Google Play is running 8 | 9 | Usage guide 10 | Play services 11 | Play Store 12 | Android Auto 13 | 14 | Google Settings 15 | %s app info 16 | Google Location Accuracy 17 | 18 | "Google’s location service is disabled. Tap “%1$s” to learn more. 19 | If you decide to keep it disabled, consider enabling “%2$s” option to improve app compatibility." 20 | 21 | Revoke the Location permission from Play services. 22 | 23 | Play services app info 24 | Play Store app info 25 | 26 | Potential issues 27 | 28 | Geolocation 29 | Location access is off for all apps. 30 | Location settings 31 | 32 | "Location requests are rerouted to the OS, but Play services still has the Location permission." 33 | 34 | 35 | "Play services needs Location permission to be set to “%s”, and “Use precise location” needs to be enabled." 36 | 37 | 38 | "Play services needs the “Wi-Fi control” permission to scan for Wi-Fi networks, which is used to estimate location. 39 | This permission can be allowed in Settings ⮕ Apps ⮕ Special app access ⮕ Wi-Fi control." 40 | 41 | 42 | "Location service assisted by Bluetooth scanning requires the “Nearby devices” permission." 43 | 44 | Always-on scanning settings 45 | Always-on Wi-Fi scanning is disabled. 46 | Always-on Bluetooth scanning is disabled. 47 | 48 | Push notifications 49 | 50 | "When device is idle, push notifications will be delayed to improve battery life. 51 | To receive them quicker, set “Battery usage” to “Unrestricted” in Play services app info." 52 | 53 | 54 | Play Store needs to update %1$s 55 | It\'s recommended to allow this. Tap to proceed. 56 | Action required in Play Store 57 | Missing permission 58 | Missing optional permission 59 | Missing app 60 | To install this app, Play Store needs access to Android/obb folder 61 | 62 | 63 | "Play services needs the “Nearby devices” permission to enable Exposure Notifications" 64 | 65 | 66 | 67 | "Play services needs the “Nearby devices“ permission to enable Nearby Share" 68 | 69 | 70 | Missing Play Games app 71 | %1$s needs the Play Games app 72 | Install 73 | 74 | Request to show a screen 75 | %s needs to show a screen. Tap to allow 76 | 77 | Play services needs the “Nearby devices” permission 78 | 79 | %1$s tried to show a notification 80 | 81 | Sandboxed Google Play crashed 82 | %1$s has crashed 83 | Tap to see the details 84 | Report to developers 85 | 86 | 87 | "Push notifications will be delayed because Play services app is not allowed to always run in the background. Tap to resolve." 88 | 89 | 90 | "Push notifications will be delayed because Play services app is not allowed to use mobile data from the background. Tap to open Play services settings, which contain mobile data usage settings." 91 | 92 | Don\'t show again 93 | 94 | This app needs the Speech Services component 95 | 96 | 97 | "To sync contacts from a Google account, Play services needs the Contacts permission. Tap to open settings." 98 | 99 | 100 | Screen capture is running 101 | 102 | %1$s requires Play services to have the “Nearby devices” permission 103 | 104 | Android Auto needs setup 105 | Go to Settings ➔ Apps ➔ Sandboxed Google Play ➔ Android Auto 106 | 107 | Play Integrity API usage 108 | Play Integrity API usage 109 | %1$s used the Play Integrity API 110 | %1$s was blocked from using the Play Integrity API 111 | More info 112 | 113 | Sign in with Google 114 | Enable Google’s credential service 115 | “Sign in with Google” requires Google’s credential service to be enabled. Tap to open settings. 116 | 117 | Location access is off for all apps 118 | 119 | -------------------------------------------------------------------------------- /config-holder/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /src/com/android/server/location/fudger/LocationFudger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.server.location.fudger; 18 | 19 | import android.annotation.Nullable; 20 | import android.location.Location; 21 | import android.location.LocationResult; 22 | import android.os.SystemClock; 23 | 24 | import com.android.internal.annotations.GuardedBy; 25 | import com.android.internal.annotations.VisibleForTesting; 26 | 27 | import java.security.SecureRandom; 28 | import java.time.Clock; 29 | import java.util.Random; 30 | 31 | /** 32 | * Contains the logic to obfuscate (fudge) locations for coarse applications. The goal is just to 33 | * prevent applications with only the coarse location permission from receiving a fine location. 34 | */ 35 | public class LocationFudger { 36 | 37 | // minimum accuracy a coarsened location can have 38 | private static final float MIN_ACCURACY_M = 200.0f; 39 | 40 | // how often random offsets are updated 41 | @VisibleForTesting 42 | static final long OFFSET_UPDATE_INTERVAL_MS = 60 * 60 * 1000; 43 | 44 | // the percentage that we change the random offset at every interval. 0.0 indicates the random 45 | // offset doesn't change. 1.0 indicates the random offset is completely replaced every interval 46 | private static final double CHANGE_PER_INTERVAL = 0.03; // 3% change 47 | 48 | // weights used to move the random offset. the goal is to iterate on the previous offset, but 49 | // keep the resulting standard deviation the same. the variance of two gaussian distributions 50 | // summed together is equal to the sum of the variance of each distribution. so some quick 51 | // algebra results in the following sqrt calculation to weight in a new offset while keeping the 52 | // final standard deviation unchanged. 53 | private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL; 54 | private static final double OLD_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT); 55 | 56 | // this number actually varies because the earth is not round, but 111,000 meters is considered 57 | // generally acceptable 58 | private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111_000; 59 | 60 | // we pick a value 1 meter away from 90.0 degrees in order to keep cosine(MAX_LATITUDE) to a 61 | // non-zero value, so that we avoid divide by zero errors 62 | private static final double MAX_LATITUDE = 63 | 90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR); 64 | 65 | private final float mAccuracyM; 66 | private final Clock mClock; 67 | private final Random mRandom; 68 | 69 | @GuardedBy("this") 70 | private double mLatitudeOffsetM; 71 | @GuardedBy("this") 72 | private double mLongitudeOffsetM; 73 | @GuardedBy("this") 74 | private long mNextUpdateRealtimeMs; 75 | 76 | @GuardedBy("this") 77 | @Nullable private Location mCachedFineLocation; 78 | @GuardedBy("this") 79 | @Nullable private Location mCachedCoarseLocation; 80 | 81 | @GuardedBy("this") 82 | @Nullable private LocationResult mCachedFineLocationResult; 83 | @GuardedBy("this") 84 | @Nullable private LocationResult mCachedCoarseLocationResult; 85 | 86 | public LocationFudger(float accuracyM) { 87 | this(accuracyM, SystemClock.elapsedRealtimeClock(), new SecureRandom()); 88 | } 89 | 90 | @VisibleForTesting 91 | LocationFudger(float accuracyM, Clock clock, Random random) { 92 | mClock = clock; 93 | mRandom = random; 94 | mAccuracyM = Math.max(accuracyM, MIN_ACCURACY_M); 95 | 96 | resetOffsets(); 97 | } 98 | 99 | /** 100 | * Resets the random offsets completely. 101 | */ 102 | public void resetOffsets() { 103 | mLatitudeOffsetM = nextRandomOffset(); 104 | mLongitudeOffsetM = nextRandomOffset(); 105 | mNextUpdateRealtimeMs = mClock.millis() + OFFSET_UPDATE_INTERVAL_MS; 106 | } 107 | 108 | /** 109 | * Coarsens a LocationResult by coarsening every location within the location result with 110 | * {@link #createCoarse(Location)}. 111 | */ 112 | public LocationResult createCoarse(LocationResult fineLocationResult) { 113 | synchronized (this) { 114 | if (fineLocationResult == mCachedFineLocationResult 115 | || fineLocationResult == mCachedCoarseLocationResult) { 116 | return mCachedCoarseLocationResult; 117 | } 118 | } 119 | 120 | LocationResult coarseLocationResult = fineLocationResult.map(this::createCoarse); 121 | 122 | synchronized (this) { 123 | mCachedFineLocationResult = fineLocationResult; 124 | mCachedCoarseLocationResult = coarseLocationResult; 125 | } 126 | 127 | return coarseLocationResult; 128 | } 129 | 130 | /** 131 | * Create a coarse location using two technique, random offsets and snap-to-grid. 132 | * 133 | * First we add a random offset to mitigate against detecting grid transitions. Without a random 134 | * offset it is possible to detect a user's position quite accurately when they cross a grid 135 | * boundary. The random offset changes very slowly over time, to mitigate against taking many 136 | * location samples and averaging them out. Second we snap-to-grid (quantize). This has the nice 137 | * property of producing stable results, and mitigating against taking many samples to average 138 | * out a random offset. 139 | */ 140 | public Location createCoarse(Location fine) { 141 | synchronized (this) { 142 | if (fine == mCachedFineLocation || fine == mCachedCoarseLocation) { 143 | return mCachedCoarseLocation; 144 | } 145 | } 146 | 147 | // update the offsets in use 148 | updateOffsets(); 149 | 150 | Location coarse = new Location(fine); 151 | 152 | // clear any fields that could leak more detailed location information 153 | coarse.removeBearing(); 154 | coarse.removeSpeed(); 155 | coarse.removeAltitude(); 156 | coarse.setExtras(null); 157 | 158 | double latitude = wrapLatitude(coarse.getLatitude()); 159 | double longitude = wrapLongitude(coarse.getLongitude()); 160 | 161 | // add offsets - update longitude first using the non-offset latitude 162 | longitude += wrapLongitude(metersToDegreesLongitude(mLongitudeOffsetM, latitude)); 163 | latitude += wrapLatitude(metersToDegreesLatitude(mLatitudeOffsetM)); 164 | 165 | // quantize location by snapping to a grid. this is the primary means of obfuscation. it 166 | // gives nice consistent results and is very effective at hiding the true location (as 167 | // long as you are not sitting on a grid boundary, which the random offsets mitigate). 168 | // 169 | // note that we quantize the latitude first, since the longitude quantization depends on 170 | // the latitude value and so leaks information about the latitude 171 | double latGranularity = metersToDegreesLatitude(mAccuracyM); 172 | latitude = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity); 173 | double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude); 174 | longitude = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity); 175 | 176 | coarse.setLatitude(latitude); 177 | coarse.setLongitude(longitude); 178 | coarse.setAccuracy(Math.max(mAccuracyM, coarse.getAccuracy())); 179 | 180 | synchronized (this) { 181 | mCachedFineLocation = fine; 182 | mCachedCoarseLocation = coarse; 183 | } 184 | 185 | return coarse; 186 | } 187 | 188 | /** 189 | * Update the random offsets over time. 190 | * 191 | * If the random offset was reset for every location fix then an application could more easily 192 | * average location results over time, especially when the location is near a grid boundary. On 193 | * the other hand if the random offset is constant then if an application finds a way to reverse 194 | * engineer the offset they would be able to detect location at grid boundaries very accurately. 195 | * So we choose a random offset and then very slowly move it, to make both approaches very hard. 196 | * The random offset does not need to be large, because snap-to-grid is the primary obfuscation 197 | * mechanism. It just needs to be large enough to stop information leakage as we cross grid 198 | * boundaries. 199 | */ 200 | private synchronized void updateOffsets() { 201 | long now = mClock.millis(); 202 | if (now < mNextUpdateRealtimeMs) { 203 | return; 204 | } 205 | 206 | mLatitudeOffsetM = (OLD_WEIGHT * mLatitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset()); 207 | mLongitudeOffsetM = (OLD_WEIGHT * mLongitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset()); 208 | mNextUpdateRealtimeMs = now + OFFSET_UPDATE_INTERVAL_MS; 209 | } 210 | 211 | private double nextRandomOffset() { 212 | return mRandom.nextGaussian() * (mAccuracyM / 4.0); 213 | } 214 | 215 | private static double wrapLatitude(double lat) { 216 | if (lat > MAX_LATITUDE) { 217 | lat = MAX_LATITUDE; 218 | } 219 | if (lat < -MAX_LATITUDE) { 220 | lat = -MAX_LATITUDE; 221 | } 222 | return lat; 223 | } 224 | 225 | private static double wrapLongitude(double lon) { 226 | lon %= 360.0; // wraps into range (-360.0, +360.0) 227 | if (lon >= 180.0) { 228 | lon -= 360.0; 229 | } 230 | if (lon < -180.0) { 231 | lon += 360.0; 232 | } 233 | return lon; 234 | } 235 | 236 | private static double metersToDegreesLatitude(double distance) { 237 | return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR; 238 | } 239 | 240 | // requires latitude since longitudinal distances change with distance from equator. 241 | private static double metersToDegreesLongitude(double distance, double lat) { 242 | return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat)); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/app/grapheneos/gmscompat/Notifications.kt: -------------------------------------------------------------------------------- 1 | package app.grapheneos.gmscompat 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.NotificationManager.IMPORTANCE_HIGH 7 | import android.app.compat.gms.GmsCompat 8 | import android.content.Intent 9 | import android.content.pm.PackageManager 10 | import android.ext.PackageId 11 | import android.net.Uri 12 | import android.os.PowerManager 13 | import android.provider.Settings 14 | import app.grapheneos.gmscompat.App.MainProcessPrefs 15 | import com.android.internal.gmscompat.GmsInfo 16 | import java.util.concurrent.atomic.AtomicInteger 17 | 18 | object Notifications { 19 | const val CH_PERSISTENT_FG_SERVICE = "persistent_fg_service" 20 | const val CH_PLAY_STORE_PENDING_USER_ACTION = "play_store_pending_user_action" 21 | const val CH_MISSING_PERMISSION = "missing_permission" 22 | const val CH_MISSING_OPTIONAL_PERMISSION = "missing_optional_permission" 23 | const val CH_MISSING_APP = "missing_app" 24 | // separate channel to allow silencing it without impacting other missing app prompts 25 | const val CH_MISSING_PLAY_GAMES_APP = "missing_play_games_app" 26 | const val CH_BACKGROUND_ACTIVITY_START = "bg_activity_start" 27 | const val CH_GMS_CRASHED = "gms_crashed" 28 | const val CH_MANAGE_PLAY_INTEGRITY_API = "app_used_play_integrity_api" 29 | const val CH_SIGN_IN_WITH_GOOGLE = "sign_in_with_google" 30 | 31 | const val ID_PERSISTENT_FG_SERVICE = 1 32 | const val ID_PLAY_STORE_PENDING_USER_ACTION = 2 33 | const val ID_PLAY_STORE_MISSING_OBB_PERMISSION = 3 34 | const val ID_GMS_CORE_MISSING_NEARBY_DEVICES_PERMISSION = 4 35 | const val ID_MISSING_APP = 6 36 | const val ID_GmsCore_POWER_EXEMPTION_PROMPT = 7 37 | const val ID_CONTACTS_SYNC_PROMPT = 8 38 | const val ID_MISSING_POST_NOTIFICATIONS_PERM = 9; 39 | const val ID_ANDROID_AUTO_NEEDS_BASELINE_PERMS = 10 40 | const val ID_GmsCore_BACKGROUND_DATA_EXEMPTION_PROMPT = 11 41 | const val ID_MANAGE_PLAY_INTEGRITY_API = 12 42 | const val ID_ENABLE_GOOGLE_CREDENTIAL_PROVIDER = 13 43 | 44 | private val uniqueNotificationId = AtomicInteger(10_000) 45 | fun generateUniqueNotificationId() = uniqueNotificationId.getAndIncrement() 46 | 47 | @JvmStatic 48 | fun createNotificationChannels() { 49 | val list = listOf( 50 | ch(CH_PERSISTENT_FG_SERVICE, R.string.persistent_fg_service_notif).apply { isBlockable = true }, 51 | ch(CH_PLAY_STORE_PENDING_USER_ACTION, R.string.play_store_pending_user_action_notif), 52 | ch(CH_MISSING_PERMISSION, R.string.missing_permission, IMPORTANCE_HIGH), 53 | ch(CH_MISSING_OPTIONAL_PERMISSION, R.string.missing_optional_permission), 54 | ch(CH_MISSING_APP, R.string.missing_app, IMPORTANCE_HIGH), 55 | ch(CH_MISSING_PLAY_GAMES_APP, R.string.notif_ch_missing_play_games_app, IMPORTANCE_HIGH), 56 | ch(CH_BACKGROUND_ACTIVITY_START, R.string.notif_channel_bg_activity_start, IMPORTANCE_HIGH), 57 | ch(CH_GMS_CRASHED, R.string.notif_ch_gms_crash, IMPORTANCE_HIGH), 58 | ch(CH_MANAGE_PLAY_INTEGRITY_API, R.string.notif_ch_manage_play_integrity_api, IMPORTANCE_HIGH).apply { isBlockable = true }, 59 | ch(CH_SIGN_IN_WITH_GOOGLE, R.string.sign_in_with_google_notif_ch, IMPORTANCE_HIGH).apply { isBlockable = true }, 60 | ) 61 | 62 | App.notificationManager().createNotificationChannels(list) 63 | } 64 | 65 | private fun ch(id: String, title: Int, importance: Int = NotificationManager.IMPORTANCE_LOW, 66 | silent: Boolean = true): NotificationChannel { 67 | val c = NotificationChannel(id, App.ctx().getText(title), importance) 68 | if (silent) { 69 | c.setSound(null, null) 70 | c.enableVibration(false) 71 | } 72 | return c 73 | } 74 | 75 | fun configurationRequired(channel: String, 76 | title: CharSequence, text: CharSequence, 77 | resolutionText: CharSequence, resolutionIntent: Intent): Notification.Builder 78 | { 79 | val pendingIntent = activityPendingIntent(resolutionIntent) 80 | val resolution = Notification.Action.Builder(null, resolutionText, pendingIntent).build() 81 | 82 | return builder(channel) 83 | .setSmallIcon(R.drawable.ic_configuration_required) 84 | .setContentTitle(title) 85 | .setContentText(text) 86 | .setStyle(Notification.BigTextStyle()) 87 | .setAutoCancel(true) 88 | .setOnlyAlertOnce(true) 89 | .addAction(resolution) 90 | } 91 | 92 | @JvmStatic 93 | fun builder(channel: String) = Notification.Builder(App.ctx(), channel) 94 | 95 | @JvmStatic 96 | fun cancel(id: Int) { 97 | App.notificationManager().cancel(id) 98 | } 99 | 100 | private var handledGmsCorePowerExemption = false 101 | 102 | fun handleGmsCorePowerExemption() { 103 | if (handledGmsCorePowerExemption) { 104 | return 105 | } 106 | handledGmsCorePowerExemption = true 107 | 108 | val ctx = App.ctx() 109 | 110 | if (ctx.packageManager.checkPermission(android.Manifest.permission.INTERNET, 111 | PackageId.GMS_CORE_NAME) != PackageManager.PERMISSION_GRANTED) { 112 | return 113 | } 114 | 115 | if (App.preferences().getBoolean(MainProcessPrefs.GmsCore_POWER_EXEMPTION_PROMPT_DISMISSED, false)) { 116 | return 117 | } 118 | 119 | val powerM = ctx.getSystemService(PowerManager::class.java)!! 120 | 121 | if (powerM.isIgnoringBatteryOptimizations(GmsInfo.PACKAGE_GMS_CORE)) { 122 | return 123 | } 124 | 125 | val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) 126 | intent.data = Uri.fromParts("package", GmsInfo.PACKAGE_GMS_CORE, null) 127 | 128 | val dontShowAgainIntent = PendingActionReceiver.makeWriteBoolPrefAndCancelNotif(ctx, 129 | MainProcessPrefs.GmsCore_POWER_EXEMPTION_PROMPT_DISMISSED, true, 130 | ID_GmsCore_POWER_EXEMPTION_PROMPT) 131 | 132 | val dontShowAgainAction = Notification.Action.Builder(null, 133 | ctx.getText(R.string.dont_show_again), dontShowAgainIntent).build() 134 | 135 | builder(CH_MISSING_OPTIONAL_PERMISSION).apply { 136 | setSmallIcon(R.drawable.ic_configuration_required) 137 | setContentTitle(R.string.missing_optional_permission) 138 | setContentText(R.string.notif_gmscore_power_exemption) 139 | setStyle(Notification.BigTextStyle()) 140 | setContentIntent(activityPendingIntent(intent)) 141 | setAutoCancel(true) 142 | addAction(dontShowAgainAction) 143 | show(ID_GmsCore_POWER_EXEMPTION_PROMPT) 144 | } 145 | } 146 | 147 | private var handledGmsCoreBgDataExemption = false 148 | 149 | fun handleGmsCoreRestrictedBackgroundDataNotif() { 150 | if (handledGmsCoreBgDataExemption) { 151 | return 152 | } 153 | handledGmsCoreBgDataExemption = true 154 | 155 | val ctx = App.ctx() 156 | 157 | if (ctx.packageManager.checkPermission(android.Manifest.permission.INTERNET, 158 | PackageId.GMS_CORE_NAME) != PackageManager.PERMISSION_GRANTED) { 159 | return 160 | } 161 | 162 | if (App.preferences().getBoolean(MainProcessPrefs.GmsCore_BACKGROUND_DATA_EXEMPTION_PROMPT_DISMISSED, false)) { 163 | return 164 | } 165 | 166 | val dontShowAgainIntent = PendingActionReceiver.makeWriteBoolPrefAndCancelNotif(ctx, 167 | MainProcessPrefs.GmsCore_BACKGROUND_DATA_EXEMPTION_PROMPT_DISMISSED, true, 168 | ID_GmsCore_BACKGROUND_DATA_EXEMPTION_PROMPT) 169 | 170 | val dontShowAgainAction = Notification.Action.Builder(null, 171 | ctx.getText(R.string.dont_show_again), dontShowAgainIntent).build() 172 | 173 | builder(CH_MISSING_OPTIONAL_PERMISSION).apply { 174 | setSmallIcon(R.drawable.ic_configuration_required) 175 | setContentTitle(R.string.missing_optional_permission) 176 | setContentText(R.string.notif_gmscore_background_data_exemption) 177 | setStyle(Notification.BigTextStyle()) 178 | setContentIntent(activityPendingIntent(gmsCoreSettings())) 179 | setAutoCancel(true) 180 | addAction(dontShowAgainAction) 181 | show(ID_GmsCore_BACKGROUND_DATA_EXEMPTION_PROMPT) 182 | } 183 | } 184 | 185 | fun handleMissingApp(channel: String, prompt: CharSequence, appPkg: String) { 186 | if (isPkgInstalled(appPkg)) { 187 | return 188 | } 189 | 190 | val ctx = App.ctx() 191 | 192 | if (!GmsCompat.isEnabledFor(GmsInfo.PACKAGE_PLAY_STORE, ctx.userId)) { 193 | return 194 | } 195 | 196 | val uri = Uri.parse("market://details?id=$appPkg") 197 | val resolution = Intent(Intent.ACTION_VIEW, uri) 198 | resolution.setPackage(GmsInfo.PACKAGE_PLAY_STORE) 199 | configurationRequired( 200 | channel, 201 | ctx.getText(R.string.missing_app), 202 | prompt, 203 | ctx.getText(R.string.install), 204 | resolution 205 | ).show(ID_MISSING_APP) 206 | } 207 | 208 | // returns null if "do not show again" is already set for this notificationId 209 | fun doNotShowAgainAction(actionId: Int, notifId: Int = actionId, prefSuffix: String = ""): Notification.Action? { 210 | val pref = MainProcessPrefs.NOTIFICATION_DO_NOT_SHOW_AGAIN_PREFIX + 211 | actionId + prefSuffix 212 | 213 | if (App.preferences().getBoolean(pref, false)) { 214 | return null 215 | } 216 | 217 | val intent = PendingActionReceiver.makeWriteBoolPrefAndCancelNotif(App.ctx(), 218 | pref, true, notifId) 219 | 220 | return Notification.Action.Builder(null, App.ctx().getText(R.string.dont_show_again), 221 | intent).build() 222 | } 223 | 224 | private var handledContactsSync = false 225 | 226 | fun handleContactsSync() { 227 | if (handledContactsSync) { 228 | return 229 | } 230 | handledContactsSync = true 231 | 232 | val id = ID_CONTACTS_SYNC_PROMPT 233 | 234 | val doNotShowAgainAction = doNotShowAgainAction(id) 235 | 236 | if (doNotShowAgainAction == null) { 237 | return 238 | } 239 | 240 | builder(CH_MISSING_OPTIONAL_PERMISSION).apply { 241 | setSmallIcon(R.drawable.ic_configuration_required) 242 | setContentTitle(R.string.missing_optional_permission) 243 | setContentText(R.string.notif_contacts_sync_prompt) 244 | setStyle(Notification.BigTextStyle()) 245 | setContentIntent(appSettingsPendingIntent(GmsInfo.PACKAGE_GMS_CORE, APP_INFO_ITEM_PERMISSIONS)) 246 | setAutoCancel(true) 247 | addAction(doNotShowAgainAction) 248 | show(id) 249 | } 250 | } 251 | } 252 | 253 | fun Notification.Builder.setContentTitle(resId: Int) { 254 | setContentTitle(App.ctx().getText(resId)) 255 | } 256 | 257 | fun Notification.Builder.setContentText(resId: Int) { 258 | setContentText(App.ctx().getText(resId)) 259 | } 260 | 261 | fun Notification.Builder.show(id: Int) { 262 | App.notificationManager().notify(id, this.build()) 263 | } 264 | -------------------------------------------------------------------------------- /gmscompat_config: -------------------------------------------------------------------------------- 1 | [[flags]] 2 | 3 | [com.google.android.gms] 4 | # disable AnomalyConfigIntentOperation, which crashes due to unavailable StatsManager 5 | 45681197 false 6 | # DeviceDoctor kills GmsCore process from an UncaughtExceptionHandler, which hides the crash from 7 | # the GmsCompat crash reporting system 8 | DeviceDoctor__devicedoctor_enabled false 9 | 10 | [com.google.android.gms.enpromo] 11 | # enpromo is a GmsCore module that shows a notification that prompts the user to enable 12 | # Exposure Notifications (en). It needs location access to determine which location-specific app 13 | # needs to be installed for Exposure Notifications to function. 14 | # Location permission can't be revoked for privileged GmsCore, it being revoked leads to a crash 15 | # (location access being disabled is handled correctly, but spoofing it would break other functionality) 16 | PromoFeature__enabled perms-none-of android.permission.ACCESS_COARSE_LOCATION false 17 | 18 | [com.google.android.gms.fido] 19 | # Note that the rest of dynamic FIDO flags are forced to their default values by force_default_flags 20 | # below for increased stability 21 | Passkeys__client_data_hash_override_for_security_keys true 22 | 23 | [com.google.android.metrics] 24 | # controls access to privileged StatsManager#getRegisteredExperimentIds() 25 | add_external_experiment_ids false 26 | 27 | [com.google.android.gms.advancedprotection#com.google.android.gms] 28 | # disable Advanced Protection, as privileged permissions are needed to enable advanced protection 29 | # via the AdvancedProtectionService. Many of the device protection features are already implemented 30 | # in GrapheneOS. 31 | 45673629 false 32 | AdvancedProtection__enable_titanium_enrollment_notification false 33 | 34 | [com.google.android.gms.nearby] 35 | # disable WearableDataListenerService, it needs privileged permissions 36 | FastPairFeature__enable_wearable_service false 37 | # use PackageManager#getNameForUid() instead of PackageManager#getPackagesForUid(), the latter needs 38 | # a shim that is currently absent 39 | FastPairConfig__sass_get_package_name_from_playback_configuration false 40 | 41 | [com.google.android.gms.cast] 42 | # Prevent crashes in GmsCore caused by calling ConnectivityManager#registerNetworkCallback too many 43 | # times when it has no INTERNET permission (there's a OS-enforced limit on the number of these callbacks). 44 | # Likely a regression, newer and older versions of GmsCore don't have this issue. 45 | # This flag makes it use a deprecated BroadcastReceiver instead. 46 | MdnsConfigs__use_connectivity_manager_for_wifi_monitor_if_possible false 47 | # CAST_CONNECTION_NOTIFY dialogs are incompatible with gmscompat code that shows a notification 48 | # prompt on background activity starts, because startActivity() calls are used for both showing and 49 | # auto-dismissing them. These dialogs are disabled by default, but were enabled with a PhenotypeFlag 50 | # update. 51 | # See https://discuss.grapheneos.org/d/1263-notification-says-google-play-services-needs-to-show-a-screen 52 | ReceiverControlChannelFeatures__enable_user_accept_dialogs false 53 | 54 | [com.google.android.gms.people] 55 | # controls access to ContactsContract$SimContacts#getSimAccounts() 56 | AccountCategoriesFeature__use_cp2_sim_account_settings perms-none-of android.permission.READ_CONTACTS false 57 | FsaDeviceContactsMoveFeature__large_contact_list_device_opt_in_disallowed perms-none-of android.permission.READ_CONTACTS false 58 | 59 | [com.google.android.gms.security] 60 | # disable TelecomTaskService, it needs privileged MODIFY_PHONE_STATE permission 61 | enable_telecom_feature false 62 | 63 | [com.google.android.gms.update] 64 | # OS update service 65 | # This flag is replaced with the gservices update_service_enabled flag since GmsCore 24.31 66 | service_enabled false 67 | update_install_enable_resume_on_reboot false 68 | 69 | [com.google.android.gms.wallet] 70 | # hiding overlay windows requires a privileged permission 71 | BenderLaunchFeatures__enable_set_hide_overlay_windows false 72 | 73 | [com.google.android.westworld] 74 | # seems to be an OS stats/analytics service, requires privileged permissions to function 75 | enabled false 76 | is_enabled false 77 | metrics_enabled false 78 | metadata_enabled false 79 | 80 | [com.google.android.gms.auth_account] 81 | # needed to prevent GmsCore from blocking Google account removal due to lacking a privileged integration into OS 82 | BugFixFeatures__fix_frp_in_r false 83 | 84 | [com.google.android.location] 85 | # these flags are needed to prevent GmsCore from ignoring results of the legacy WifiManager#startScan() 86 | # method, which doesn't have an unprivileged modern version. It's used for location estimation based 87 | # on nearby Wi-Fi access points 88 | d_psl false 89 | fwm true 90 | 91 | [com.google.android.gms.kidssettings#com.google.android.gms] 92 | SeparateApkFeature__disable_separate_apk_if_not_used false 93 | 94 | [com.google.android.projection.gearhead] 95 | # prevent Android Auto from trying to query a nonexistent Assistant service in the Google app 96 | Assistant__dodgeboost_initial_handshake_enabled true 97 | 98 | [gservices] 99 | # make sure PhenotypeFlags are overridable by Gservices flags. Overriding Play Store (finsky) PhenotypeFlags 100 | # directly is much more complex than overriding Gservices flags. 101 | finsky.kill_switch_phenotype_gservices_check set-string 0 102 | # disable GmsCore OS update service 103 | update_service_enabled set-string 0 104 | 105 | [[stubs]] 106 | 107 | [android.app.ActivityManager] 108 | addOnUidImportanceListener void 109 | # 2 is ProcessState.IMPORTANCE_TOP 110 | getPackageImportance int 2 111 | 112 | [android.app.admin.DevicePolicyManager] 113 | isDeviceProvisioned true 114 | isDeviceProvisioningConfigApplied true 115 | getDeviceOwnerComponentOnAnyUser null 116 | getDeviceOwnerNameOnAnyUser nullString 117 | getProfileOwnerNameAsUser nullString 118 | notifyPendingSystemUpdate void 119 | 120 | [android.app.backup.BackupManager] 121 | isBackupEnabled false 122 | isBackupServiceActive false 123 | 124 | [android.app.supervision.SupervisionManager] 125 | isSupervisionEnabled false 126 | 127 | [android.app.usage.UsageStatsManager] 128 | # 10 is STANDBY_BUCKET_ACTIVE 129 | getAppStandbyBucket int 10 130 | 131 | [android.bluetooth.BluetoothA2dp] 132 | setConnectionPolicy false 133 | 134 | [android.bluetooth.BluetoothAdapter] 135 | disable false 136 | enable false 137 | # 20 is SCAN_MODE_NONE 138 | getScanMode int 20 139 | getName nullString 140 | # 02:00:00:00:00:00 is DEFAULT_MAC_ADDRESS 141 | getAddress String 02:00:00:00:00:00 142 | listenUsingInsecureL2capChannel throw java.io.IOException 143 | getProfileProxy false 144 | registerBluetoothConnectionCallback default 145 | unregisterBluetoothConnectionCallback default 146 | 147 | [android.bluetooth.BluetoothDevice] 148 | getIdentityAddress default 149 | getMetadata default 150 | setMetadata default 151 | setPairingConfirmation default 152 | setSilenceMode default 153 | 154 | [android.bluetooth.BluetoothHapClient] 155 | registerCallback void 156 | unregisterCallback void 157 | 158 | [android.bluetooth.BluetoothLeAudio] 159 | registerCallback void 160 | unregisterCallback void 161 | 162 | [android.bluetooth.BluetoothLeBroadcast] 163 | registerCallback void 164 | unregisterCallback void 165 | 166 | [android.bluetooth.BluetoothLeBroadcastAssistant] 167 | getConnectedDevices emptyList 168 | registerCallback void 169 | unregisterCallback void 170 | 171 | [android.bluetooth.BluetoothManager] 172 | openGattServer default 173 | 174 | [android.bluetooth.le.BluetoothLeAdvertiser] 175 | startAdvertisingSet default 176 | 177 | [android.content.ContentResolver] 178 | registerContentObserver void 179 | 180 | [android.content.pm.verify.domain.DomainVerificationManager] 181 | queryValidVerificationPackageNames emptyList 182 | 183 | [android.hardware.usb.UsbManager] 184 | getPorts emptyList 185 | grantPermission void 186 | setCurrentFunctions void 187 | 188 | [android.location.LocationManager] 189 | getLastKnownLocation null 190 | registerGnssMeasurementsCallback false 191 | registerGnssStatusCallback false 192 | setExtraLocationControllerPackage void 193 | setExtraLocationControllerPackageEnabled void 194 | 195 | [android.media.AudioManager] 196 | muteAwaitConnection void 197 | getMutingExpectedDevice null 198 | registerMuteAwaitConnectionCallback void 199 | unregisterMuteAwaitConnectionCallback void 200 | 201 | [android.media.MediaRouter2] 202 | getInstance null 203 | 204 | [android.media.session.MediaSessionManager] 205 | getActiveSessions emptyList 206 | addOnActiveSessionsChangedListener void 207 | 208 | [android.net.wifi.WifiManager] 209 | getPrivilegedConfiguredNetworks emptyList 210 | getAllMatchingWifiConfigs emptyList 211 | getMatchingOsuProviders emptyMap 212 | getMatchingPasspointConfigsForOsuProviders emptyMap 213 | getSoftApConfiguration null 214 | getCurrentNetwork null 215 | reconnect false 216 | 217 | [android.nfc.NfcAdapter] 218 | disable false 219 | 220 | [android.os.DropBoxManager] 221 | getNextEntry null 222 | 223 | [android.os.SystemVibratorManager$SingleVibrator] 224 | addVibratorStateListener void 225 | 226 | [android.os.UserManager] 227 | getSeedAccountOptions null 228 | getUserName String Owner 229 | getUserRestrictionSources emptyList 230 | isMainUser true 231 | 232 | [android.safetycenter.SafetyCenterManager] 233 | isSafetyCenterEnabled false 234 | 235 | [android.security.advancedprotection.AdvancedProtectionManager] 236 | getAdvancedProtectionFeatures emptyList 237 | setAdvancedProtectionEnabled void 238 | logDialogShown void 239 | 240 | [android.security.keystore.recovery.RecoveryController] 241 | initRecoveryService void 242 | setServerParams void 243 | 244 | [android.telecom.TelecomManager] 245 | getAllPhoneAccountHandles emptyList 246 | getCallCapablePhoneAccounts emptyList 247 | getPhoneAccount null 248 | getSelfManagedPhoneAccounts emptyList 249 | getUserSelectedOutgoingPhoneAccount null 250 | placeCall void 251 | 252 | [android.telephony.SubscriptionManager] 253 | getActiveSubscriptionIdList emptyIntArray 254 | getPhoneNumber emptyString 255 | 256 | [android.telephony.TelephonyManager] 257 | getDeviceId nullString 258 | getImei nullString 259 | getMeid nullString 260 | # 0 is NETWORK_TYPE_UNKNOWN 261 | getDataNetworkType int 0 262 | getNetworkType int 0 263 | getVoiceNetworkType int 0 264 | # 0 is CALL_STATE_IDLE 265 | getCallState int 0 266 | getCallStateForSubscription int 0 267 | getSimSerialNumber nullString 268 | getUiccSlotsInfo nullArray 269 | getSubscriberId nullString 270 | getLine1Number nullString 271 | getAllCellInfo emptyList 272 | isIccLockEnabled false 273 | getGroupIdLevel1 nullString 274 | unregisterTelephonyCallback void 275 | getServiceState null 276 | getVoiceMailNumber nullString 277 | 278 | [com.android.internal.gmscompat.sysservice.GmcPackageManager] 279 | getPackagesForUid nullArray 280 | 281 | [[versionMap]] 282 | # section name is gmscompat version, section contents are max supported versions of GmsCore and Play Store 283 | [1000] 284 | # GmsCore 24.22 requires GmsCompat 1008 285 | 242199999 84459999 286 | 287 | [1008] 288 | 245099999 84459999 289 | 290 | [[stubs_12.1]] 291 | 292 | [android.bluetooth.BluetoothDevice] 293 | getMetadata null 294 | setMetadata false 295 | setSilenceMode false 296 | 297 | [android.bluetooth.BluetoothAdapter] 298 | registerBluetoothConnectionCallback false 299 | unregisterBluetoothConnectionCallback false 300 | 301 | [android.content.ContextWrapper] 302 | sendBroadcastAsUser int 0 303 | 304 | [android.os.UserManager] 305 | isUserOfType false 306 | 307 | [[GmsServiceBroker_self_permission_bypass]] 308 | [1003] 309 | # DROID_GUARD (25) service can work without the Phone permission, despite requiring it to be granted. 310 | # This service is used for the Play Integrity API. 311 | 25 android.permission.READ_PHONE_STATE 312 | 313 | [[force_default_flags]] 314 | [com.google.android.gms.fido] 315 | .+ 316 | 317 | [[force_ComponentEnabledSettings]] 318 | [com.google.android.gms] 319 | # causes chain crashes on Pixel Tablet due to the missing Dock Manager app 320 | disable com.google.android.gms.homegraph.PersistentListenerService 321 | disable com.google.android.gms.update.SystemUpdateGcmTaskService 322 | disable com.google.android.gms.update.SystemUpdatePersistentListenerService 323 | disable com.google.android.gms.update.SystemUpdateService 324 | disable com.google.android.gms.update.UpdateJobService 325 | 326 | [[spoof_self_permission_checks_v2]] 327 | 328 | [com.google.android.projection.gearhead] 329 | android.permission.ACCESS_COARSE_LOCATION 330 | android.permission.ACCESS_FINE_LOCATION 331 | android.permission.ASSOCIATE_COMPANION_DEVICES 332 | android.permission.BLUETOOTH_CONNECT 333 | android.permission.BLUETOOTH_SCAN 334 | android.permission.CALL_PHONE 335 | android.permission.MANAGE_USB 336 | android.permission.READ_PHONE_STATE 337 | android.permission.READ_PRIVILEGED_PHONE_STATE 338 | android.permission.RECORD_AUDIO 339 | --------------------------------------------------------------------------------