1 /* 2 * Copyright (C) 2011 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 androidx.core.content; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY; 20 import static androidx.core.content.UnusedAppRestrictionsConstants.API_30; 21 import static androidx.core.content.UnusedAppRestrictionsConstants.API_30_BACKPORT; 22 import static androidx.core.content.UnusedAppRestrictionsConstants.API_31; 23 import static androidx.core.content.UnusedAppRestrictionsConstants.DISABLED; 24 import static androidx.core.content.UnusedAppRestrictionsConstants.ERROR; 25 import static androidx.core.content.UnusedAppRestrictionsConstants.FEATURE_NOT_AVAILABLE; 26 27 import static java.lang.annotation.RetentionPolicy.SOURCE; 28 29 import android.annotation.SuppressLint; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ResolveInfo; 34 import android.net.Uri; 35 import android.os.Build; 36 import android.util.Log; 37 38 import androidx.annotation.IntDef; 39 import androidx.annotation.RequiresApi; 40 import androidx.annotation.RestrictTo; 41 import androidx.concurrent.futures.ResolvableFuture; 42 import androidx.core.os.UserManagerCompat; 43 44 import com.google.common.util.concurrent.ListenableFuture; 45 46 import org.jspecify.annotations.NonNull; 47 import org.jspecify.annotations.Nullable; 48 49 import java.lang.annotation.Retention; 50 import java.util.List; 51 import java.util.concurrent.Executors; 52 53 /** 54 * Helper for accessing features in {@link PackageManager}. 55 */ 56 public final class PackageManagerCompat { PackageManagerCompat()57 private PackageManagerCompat() { 58 /* Hide constructor */ 59 } 60 61 @RestrictTo(LIBRARY) 62 public static final String LOG_TAG = "PackageManagerCompat"; 63 64 /** 65 * Activity action: creates an intent to redirect the user to UI to turn on/off their 66 * permission revocation settings. 67 */ 68 @SuppressLint("ActionValue") 69 public static final String ACTION_PERMISSION_REVOCATION_SETTINGS = 70 "android.intent.action.AUTO_REVOKE_PERMISSIONS"; 71 72 /** 73 * The status of Unused App Restrictions for this app. 74 */ 75 @IntDef({ERROR, FEATURE_NOT_AVAILABLE, DISABLED, API_30_BACKPORT, API_30, API_31}) 76 @Retention(SOURCE) 77 @RestrictTo(LIBRARY) 78 public @interface UnusedAppRestrictionsStatus { 79 } 80 81 /** 82 * Returns the status of Unused App Restrictions for the current application. 83 * In other words, whether the features are available and if so, enabled for the application. 84 * 85 * The returned value is a ListenableFuture with an Integer corresponding to a value in 86 * {@link UnusedAppRestrictionsConstants}. 87 * 88 * The possible values are as follows: 89 * <ul> 90 * <li>{@link UnusedAppRestrictionsConstants#ERROR}: an error occurred when fetching 91 * the availability and status of Unused App Restrictions. Check the logs for 92 * the reason (e.g. if the app's target SDK version < 30 or the user is in locked device 93 * boot mode).</li> 94 * <li>{@link UnusedAppRestrictionsConstants#FEATURE_NOT_AVAILABLE}: there are no 95 * available Unused App Restrictions for this app.</li> 96 * <li>{@link UnusedAppRestrictionsConstants#DISABLED}: any available Unused App 97 * Restrictions on the device are disabled for this app.</li> 98 * <li>{@link UnusedAppRestrictionsConstants#API_30_BACKPORT}: Unused App Restrictions 99 * introduced by Android API 30, and since made available on earlier (API 23-29) devices 100 * are enabled for this app: 101 * <a href="https://developer.android.com/training/permissions/requesting#auto-reset-permissions-unused-apps" 102 * >permission auto-reset</a>.</li> 103 * <li>{@link UnusedAppRestrictionsConstants#API_30}: Unused App Restrictions introduced 104 * by Android API 30 are enabled for this app: 105 * <a href="https://developer.android.com/training/permissions/requesting#auto-reset-permissions-unused-apps" 106 * >permission auto-reset</a>.</li> 107 * <li>{@link UnusedAppRestrictionsConstants#API_31}: Unused App Restrictions introduced 108 * by Android API 31 are enabled for this app: 109 * <a href="https://developer.android.com/training/permissions/requesting#auto-reset-permissions-unused-apps" 110 * >permission auto-reset</a> and 111 * <a href="https://developer.android.com/about/versions/12/behavior-changes-12#app-hibernation" 112 * >app hibernation</a>.</li> 113 * </ul> 114 * 115 * Compatibility behavior: 116 * <ul> 117 * <li>SDK 31 and above, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this API 118 * will return {@link UnusedAppRestrictionsConstants#DISABLED}. Else, it will return 119 * {@link UnusedAppRestrictionsConstants#API_31}.</li> 120 * <li>SDK 30, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this API will return 121 * {@link UnusedAppRestrictionsConstants#DISABLED}. Else, it will return 122 * {@link UnusedAppRestrictionsConstants#API_30}.</li> 123 * <li>SDK 23 through 29, if there exists an app with the Verifier role that can resolve the 124 * {@code Intent.ACTION_AUTO_REVOKE_PERMISSIONS} action, then this API will return 125 * {@link UnusedAppRestrictionsConstants#API_30_BACKPORT} if Unused App Restrictions are 126 * enabled and {@link UnusedAppRestrictionsConstants#DISABLED} if disabled. Else, it will 127 * return {@link UnusedAppRestrictionsConstants#FEATURE_NOT_AVAILABLE}. 128 * <li>SDK 22 and below, this method always returns 129 * {@link UnusedAppRestrictionsConstants#FEATURE_NOT_AVAILABLE} as runtime permissions did 130 * not exist yet. 131 * </ul> 132 */ getUnusedAppRestrictionsStatus( @onNull Context context)133 public static @NonNull ListenableFuture<Integer> getUnusedAppRestrictionsStatus( 134 @NonNull Context context) { 135 ResolvableFuture<Integer> resultFuture = ResolvableFuture.create(); 136 // If the user is in locked direct boot mode, return error as we cannot access the 137 // unused app restriction settings. 138 if (!UserManagerCompat.isUserUnlocked(context)) { 139 resultFuture.set(ERROR); 140 Log.e(LOG_TAG, "User is in locked direct boot mode"); 141 return resultFuture; 142 } 143 144 if (!areUnusedAppRestrictionsAvailable(context.getPackageManager())) { 145 resultFuture.set(FEATURE_NOT_AVAILABLE); 146 return resultFuture; 147 } 148 149 int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; 150 151 if (targetSdkVersion < Build.VERSION_CODES.R) { 152 resultFuture.set(ERROR); 153 Log.e(LOG_TAG, "Target SDK version below API 30"); 154 return resultFuture; 155 } 156 157 // TODO: replace with VERSION_CODES.S once it's defined 158 if (Build.VERSION.SDK_INT >= 31) { 159 if (Api30Impl.areUnusedAppRestrictionsEnabled(context)) { 160 // API 31 unused app restrictions are only available for apps targeting API 31+. 161 // For apps targeting API 30-, API 30 unused app restrictions will be used instead. 162 resultFuture.set(targetSdkVersion >= 31 ? API_31 : API_30); 163 } else { 164 resultFuture.set(DISABLED); 165 } 166 return resultFuture; 167 } 168 169 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { 170 resultFuture.set( 171 Api30Impl.areUnusedAppRestrictionsEnabled(context) 172 ? API_30 173 : DISABLED); 174 return resultFuture; 175 } 176 177 UnusedAppRestrictionsBackportServiceConnection backportServiceConnection = 178 new UnusedAppRestrictionsBackportServiceConnection(context); 179 180 // Keep the connection object alive until the async operation completes, and then 181 // disconnect it. 182 resultFuture.addListener( 183 backportServiceConnection::disconnectFromService, 184 Executors.newSingleThreadExecutor()); 185 186 // Start binding the service and fetch the result 187 backportServiceConnection.connectAndFetchResult(resultFuture); 188 189 return resultFuture; 190 } 191 192 /** 193 * Returns whether any Unused App Restrictions are available on the device. 194 * 195 */ 196 @RestrictTo(LIBRARY) areUnusedAppRestrictionsAvailable( @onNull PackageManager packageManager)197 public static boolean areUnusedAppRestrictionsAvailable( 198 @NonNull PackageManager packageManager) { 199 boolean restrictionsBuiltIntoOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; 200 boolean isOsMThroughQ = 201 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 202 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.R); 203 boolean hasBackportFeature = getPermissionRevocationVerifierApp(packageManager) != null; 204 205 return restrictionsBuiltIntoOs || (isOsMThroughQ && hasBackportFeature); 206 } 207 208 /** 209 * Returns the package name of the one and only Verifier on the device that can support 210 * permission revocation. If none exist, this will return {@code null}. Likewise, if multiple 211 * Verifiers exist, this method will return the first Verifier's package name. 212 * 213 */ 214 @RestrictTo(LIBRARY) 215 @SuppressWarnings("deprecation") getPermissionRevocationVerifierApp( @onNull PackageManager packageManager)216 public static @Nullable String getPermissionRevocationVerifierApp( 217 @NonNull PackageManager packageManager) { 218 Intent permissionRevocationSettingsIntent = 219 new Intent(ACTION_PERMISSION_REVOCATION_SETTINGS) 220 .setData(Uri.fromParts( 221 "package", "com.example", /* fragment= */ null)); 222 List<ResolveInfo> intentResolvers = 223 packageManager.queryIntentActivities( 224 permissionRevocationSettingsIntent, /* flags= */ 0); 225 226 String verifierPackageName = null; 227 228 for (ResolveInfo intentResolver: intentResolvers) { 229 String packageName = intentResolver.activityInfo.packageName; 230 if (packageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT", 231 packageName) != PackageManager.PERMISSION_GRANTED) { 232 continue; 233 } 234 235 if (verifierPackageName != null) { 236 // This shouldn't happen, but we fail gracefully nonetheless and avoid throwing an 237 // exception, instead returning the first package name with the Verifier role 238 // that we found. 239 return verifierPackageName; 240 } 241 verifierPackageName = packageName; 242 } 243 244 return verifierPackageName; 245 } 246 247 /** 248 * We create this static class to avoid Class Verification Failures from referencing a method 249 * only added in Android R. 250 * 251 * <p>Gating references on SDK checks does not address class verification failures, hence the 252 * need for this inner class. 253 */ 254 @RequiresApi(Build.VERSION_CODES.R) 255 private static class Api30Impl { Api30Impl()256 private Api30Impl() {} areUnusedAppRestrictionsEnabled(@onNull Context context)257 static boolean areUnusedAppRestrictionsEnabled(@NonNull Context context) { 258 // If the app is allowlisted, that means that it is exempt from Unused App Restrictions, 259 // and thus the features are _disabled_. 260 return !context.getPackageManager().isAutoRevokeWhitelisted(); 261 } 262 } 263 } 264