├── 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 |
--------------------------------------------------------------------------------