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