/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.core.content; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.core.content.UnusedAppRestrictionsConstants.API_30; import static androidx.core.content.UnusedAppRestrictionsConstants.API_30_BACKPORT; import static androidx.core.content.UnusedAppRestrictionsConstants.API_31; import static androidx.core.content.UnusedAppRestrictionsConstants.DISABLED; import static androidx.core.content.UnusedAppRestrictionsConstants.ERROR; import static androidx.core.content.UnusedAppRestrictionsConstants.FEATURE_NOT_AVAILABLE; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; import android.util.Log; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.concurrent.futures.ResolvableFuture; import androidx.core.os.UserManagerCompat; import com.google.common.util.concurrent.ListenableFuture; import java.lang.annotation.Retention; import java.util.List; import java.util.concurrent.Executors; /** * Helper for accessing features in {@link PackageManager}. */ public final class PackageManagerCompat { private PackageManagerCompat() { /* Hide constructor */ } /** @hide */ @RestrictTo(LIBRARY) public static final String LOG_TAG = "PackageManagerCompat"; /** * Activity action: creates an intent to redirect the user to UI to turn on/off their * permission revocation settings. */ @SuppressLint("ActionValue") public static final String ACTION_PERMISSION_REVOCATION_SETTINGS = "android.intent.action.AUTO_REVOKE_PERMISSIONS"; /** * The status of Unused App Restrictions for this app. * @hide */ @IntDef({ERROR, FEATURE_NOT_AVAILABLE, DISABLED, API_30_BACKPORT, API_30, API_31}) @Retention(SOURCE) @RestrictTo(LIBRARY) public @interface UnusedAppRestrictionsStatus { } /** * Returns the status of Unused App Restrictions for the current application. * In other words, whether the features are available and if so, enabled for the application. * * The returned value is a ListenableFuture with an Integer corresponding to a value in * {@link UnusedAppRestrictionsConstants}. * * The possible values are as follows: * * * Compatibility behavior: * */ @NonNull public static ListenableFuture getUnusedAppRestrictionsStatus( @NonNull Context context) { ResolvableFuture resultFuture = ResolvableFuture.create(); // If the user is in locked direct boot mode, return error as we cannot access the // unused app restriction settings. if (!UserManagerCompat.isUserUnlocked(context)) { resultFuture.set(ERROR); Log.e(LOG_TAG, "User is in locked direct boot mode"); return resultFuture; } if (!areUnusedAppRestrictionsAvailable(context.getPackageManager())) { resultFuture.set(FEATURE_NOT_AVAILABLE); return resultFuture; } int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; if (targetSdkVersion < Build.VERSION_CODES.R) { resultFuture.set(ERROR); Log.e(LOG_TAG, "Target SDK version below API 30"); return resultFuture; } // TODO: replace with VERSION_CODES.S once it's defined if (Build.VERSION.SDK_INT >= 31) { if (Api30Impl.areUnusedAppRestrictionsEnabled(context)) { // API 31 unused app restrictions are only available for apps targeting API 31+. // For apps targeting API 30-, API 30 unused app restrictions will be used instead. resultFuture.set(targetSdkVersion >= 31 ? API_31 : API_30); } else { resultFuture.set(DISABLED); } return resultFuture; } if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { resultFuture.set( Api30Impl.areUnusedAppRestrictionsEnabled(context) ? API_30 : DISABLED); return resultFuture; } UnusedAppRestrictionsBackportServiceConnection backportServiceConnection = new UnusedAppRestrictionsBackportServiceConnection(context); // Keep the connection object alive until the async operation completes, and then // disconnect it. resultFuture.addListener( backportServiceConnection::disconnectFromService, Executors.newSingleThreadExecutor()); // Start binding the service and fetch the result backportServiceConnection.connectAndFetchResult(resultFuture); return resultFuture; } /** * Returns whether any Unused App Restrictions are available on the device. * * @hide */ @RestrictTo(LIBRARY) public static boolean areUnusedAppRestrictionsAvailable( @NonNull PackageManager packageManager) { boolean restrictionsBuiltIntoOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; boolean isOsMThroughQ = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) && (Build.VERSION.SDK_INT < Build.VERSION_CODES.R); boolean hasBackportFeature = getPermissionRevocationVerifierApp(packageManager) != null; return restrictionsBuiltIntoOs || (isOsMThroughQ && hasBackportFeature); } /** * Returns the package name of the one and only Verifier on the device that can support * permission revocation. If none exist, this will return {@code null}. Likewise, if multiple * Verifiers exist, this method will return the first Verifier's package name. * * @hide */ @Nullable @RestrictTo(LIBRARY) @SuppressWarnings("deprecation") public static String getPermissionRevocationVerifierApp( @NonNull PackageManager packageManager) { Intent permissionRevocationSettingsIntent = new Intent(ACTION_PERMISSION_REVOCATION_SETTINGS) .setData(Uri.fromParts( "package", "com.example", /* fragment= */ null)); List intentResolvers = packageManager.queryIntentActivities( permissionRevocationSettingsIntent, /* flags= */ 0); String verifierPackageName = null; for (ResolveInfo intentResolver: intentResolvers) { String packageName = intentResolver.activityInfo.packageName; if (packageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT", packageName) != PackageManager.PERMISSION_GRANTED) { continue; } if (verifierPackageName != null) { // This shouldn't happen, but we fail gracefully nonetheless and avoid throwing an // exception, instead returning the first package name with the Verifier role // that we found. return verifierPackageName; } verifierPackageName = packageName; } return verifierPackageName; } /** * We create this static class to avoid Class Verification Failures from referencing a method * only added in Android R. * *

Gating references on SDK checks does not address class verification failures, hence the * need for this inner class. */ @RequiresApi(Build.VERSION_CODES.R) private static class Api30Impl { private Api30Impl() {} static boolean areUnusedAppRestrictionsEnabled(@NonNull Context context) { // If the app is allowlisted, that means that it is exempt from Unused App Restrictions, // and thus the features are _disabled_. return !context.getPackageManager().isAutoRevokeWhitelisted(); } } }