1 /*
2  * Copyright 2021 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 android.content.pm.PackageManager.PERMISSION_DENIED;
20 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
21 import static android.os.Build.VERSION_CODES.LOLLIPOP;
22 import static android.os.Build.VERSION_CODES.M;
23 import static android.os.Build.VERSION_CODES.N;
24 import static android.os.Build.VERSION_CODES.Q;
25 import static android.os.Build.VERSION_CODES.R;
26 
27 import static androidx.core.content.UnusedAppRestrictionsBackportService.ACTION_UNUSED_APP_RESTRICTIONS_BACKPORT_CONNECTION;
28 import static androidx.core.content.UnusedAppRestrictionsConstants.API_30;
29 import static androidx.core.content.UnusedAppRestrictionsConstants.API_31;
30 import static androidx.core.content.UnusedAppRestrictionsConstants.DISABLED;
31 import static androidx.core.content.UnusedAppRestrictionsConstants.ERROR;
32 import static androidx.core.content.UnusedAppRestrictionsConstants.FEATURE_NOT_AVAILABLE;
33 
34 import static com.google.common.truth.Truth.assertThat;
35 
36 import static org.mockito.ArgumentMatchers.any;
37 import static org.mockito.ArgumentMatchers.eq;
38 import static org.mockito.ArgumentMatchers.nullable;
39 import static org.mockito.Mockito.mock;
40 import static org.mockito.Mockito.spy;
41 import static org.mockito.Mockito.verify;
42 import static org.mockito.Mockito.when;
43 
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.pm.ActivityInfo;
47 import android.content.pm.ApplicationInfo;
48 import android.content.pm.PackageManager;
49 import android.content.pm.ProviderInfo;
50 import android.content.pm.ResolveInfo;
51 import android.os.UserManager;
52 
53 import androidx.test.core.app.ApplicationProvider;
54 import androidx.test.ext.junit.runners.AndroidJUnit4;
55 import androidx.test.filters.SdkSuppress;
56 import androidx.test.filters.SmallTest;
57 
58 import com.google.common.util.concurrent.ListenableFuture;
59 
60 import org.junit.Before;
61 import org.junit.Test;
62 import org.junit.runner.RunWith;
63 import org.mockito.ArgumentCaptor;
64 
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.List;
68 
69 @SmallTest
70 @RunWith(AndroidJUnit4.class)
71 /** Tests for {@link PackageManagerCompat}. */
72 public class PackageManagerCompatTest {
73 
74     private Context mContext;
75     private final PackageManager mPackageManager = mock(PackageManager.class);
76     private static final String VERIFIER_PACKAGE_NAME = "verifier.package.name";
77     private static final String VERIFIER_PACKAGE_NAME2 = "verifier.package.name.2";
78     private static final String NON_VERIFIER_PACKAGE_NAME = "non.verifier.package.name";
79     private final ArgumentCaptor<Intent> mIntentCaptor = ArgumentCaptor.forClass(Intent.class);
80 
81     @Before
setUp()82     public void setUp() {
83         mContext = spy(ApplicationProvider.getApplicationContext());
84         when(mContext.getPackageManager()).thenReturn(mPackageManager);
85     }
86 
87     @Test
88     // UserManagerCompat#isUserUnlocked always returns true if on a pre-N OS version
89     @SdkSuppress(minSdkVersion = N)
getUnusedAppRestrictionsStatus_whenLockedDirectBootMode_returnsErrorStatus()90     public void getUnusedAppRestrictionsStatus_whenLockedDirectBootMode_returnsErrorStatus()
91             throws Exception {
92         UserManager userManager = mock(UserManager.class);
93         when(mContext.getSystemService(UserManager.class)).thenReturn(userManager);
94         when(userManager.isUserUnlocked()).thenReturn(false);
95 
96         ListenableFuture<Integer> resultFuture =
97                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
98 
99         assertThat(resultFuture.get()).isEqualTo(ERROR);
100     }
101 
102     @Test
103     @SdkSuppress(minSdkVersion = R)
getUnusedAppRestrictionsStatus_api30Plus_preApi30App_returnsErrorStatus()104     public void getUnusedAppRestrictionsStatus_api30Plus_preApi30App_returnsErrorStatus()
105             throws Exception {
106         ApplicationInfo appInfo = new ApplicationInfo();
107         // Mark the application as targeting pre API 30
108         appInfo.targetSdkVersion = Q;
109         when(mContext.getApplicationInfo()).thenReturn(appInfo);
110 
111         ListenableFuture<Integer> resultFuture =
112                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
113 
114         assertThat(resultFuture.get()).isEqualTo(ERROR);
115     }
116 
117     @Test
118     @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
getUnusedAppRestrictionsStatus_preApi30_preApi30App_returnsErrorStatus()119     public void getUnusedAppRestrictionsStatus_preApi30_preApi30App_returnsErrorStatus()
120             throws Exception {
121         ApplicationInfo appInfo = new ApplicationInfo();
122         // Mark the application as targeting pre API 30
123         appInfo.targetSdkVersion = Q;
124         when(mContext.getApplicationInfo()).thenReturn(appInfo);
125         setupPermissionRevocationApps(mPackageManager, Arrays.asList(VERIFIER_PACKAGE_NAME));
126         // Set this app as the Verifier on the device
127         when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
128                 VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_GRANTED);
129 
130         ListenableFuture<Integer> resultFuture =
131                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
132 
133         assertThat(resultFuture.get()).isEqualTo(ERROR);
134     }
135 
136     @Test
137     @SdkSuppress(minSdkVersion = 31)
getUnusedAppRestrictionsStatus_api31Plus_api31App_disabled_returnsDisabledStatus()138     public void getUnusedAppRestrictionsStatus_api31Plus_api31App_disabled_returnsDisabledStatus()
139             throws Exception {
140         ApplicationInfo appInfo = new ApplicationInfo();
141         // Mark the application as targeting API 31
142         appInfo.targetSdkVersion = 31;
143         when(mContext.getApplicationInfo()).thenReturn(appInfo);
144         // Mark the application as exempt from app hibernation, so the feature is disabled
145         when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(true);
146 
147         ListenableFuture<Integer> resultFuture =
148                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
149 
150         assertThat(resultFuture.get()).isEqualTo(DISABLED);
151     }
152 
153     @Test
154     @SdkSuppress(minSdkVersion = 31)
getUnusedAppRestrictionsStatus_api31Plus_api31App_enabled_returnsApi31Status()155     public void getUnusedAppRestrictionsStatus_api31Plus_api31App_enabled_returnsApi31Status()
156             throws Exception {
157         ApplicationInfo appInfo = new ApplicationInfo();
158         // Mark the application as targeting API 31
159         appInfo.targetSdkVersion = 31;
160         when(mContext.getApplicationInfo()).thenReturn(appInfo);
161         // Mark the application as _not_ exempt from app hibernation, so the feature is enabled
162         when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(false);
163 
164         ListenableFuture<Integer> resultFuture =
165                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
166 
167         assertThat(resultFuture.get()).isEqualTo(API_31);
168     }
169 
170     @Test
171     @SdkSuppress(minSdkVersion = 31)
getUnusedAppRestrictionsStatus_api31Plus_api30App_enabled_returnsApi30Status()172     public void getUnusedAppRestrictionsStatus_api31Plus_api30App_enabled_returnsApi30Status()
173             throws Exception {
174         ApplicationInfo appInfo = new ApplicationInfo();
175         // Mark the application as targeting below API 31
176         appInfo.targetSdkVersion = R;
177         when(mContext.getApplicationInfo()).thenReturn(appInfo);
178         // Mark the application as _not_ exempt from app hibernation, so the feature is enabled
179         when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(false);
180 
181         ListenableFuture<Integer> resultFuture =
182                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
183 
184         assertThat(resultFuture.get()).isEqualTo(API_30);
185     }
186 
187     @Test
188     @SdkSuppress(minSdkVersion = R, maxSdkVersion = R)
getUnusedAppRestrictionsStatus_api30_disabled_returnsDisabledStatus()189     public void getUnusedAppRestrictionsStatus_api30_disabled_returnsDisabledStatus()
190             throws Exception {
191         ApplicationInfo appInfo = new ApplicationInfo();
192         // Mark the application as targeting API 30+
193         appInfo.targetSdkVersion = R;
194         when(mContext.getApplicationInfo()).thenReturn(appInfo);
195         // Mark the application as exempt from permission revocation, so the feature is disabled
196         when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(true);
197 
198         ListenableFuture<Integer> resultFuture =
199                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
200 
201         assertThat(resultFuture.get()).isEqualTo(DISABLED);
202     }
203 
204     @Test
205     @SdkSuppress(minSdkVersion = R)
getUnusedAppRestrictionsStatus_api30Plus_enabled_returnsApi30Status()206     public void getUnusedAppRestrictionsStatus_api30Plus_enabled_returnsApi30Status()
207             throws Exception {
208         ApplicationInfo appInfo = new ApplicationInfo();
209         // Mark the application as targeting API 30+
210         appInfo.targetSdkVersion = R;
211         when(mContext.getApplicationInfo()).thenReturn(appInfo);
212         // Mark the application as _not_ exempt from permission revocation, so the feature is
213         // enabled
214         when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(false);
215 
216         ListenableFuture<Integer> resultFuture =
217                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
218 
219         assertThat(resultFuture.get()).isEqualTo(API_30);
220     }
221 
222     @Test
223     @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
getUnusedAppRestrictionsStatus_preApi30_noRevocationApp_returnsFeatureNotAvailable()224     public void getUnusedAppRestrictionsStatus_preApi30_noRevocationApp_returnsFeatureNotAvailable()
225             throws Exception {
226         // Don't install an app that can resolve the permission auto-revocation intent
227 
228         ListenableFuture<Integer> resultFuture =
229                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
230 
231         assertThat(resultFuture.get()).isEqualTo(FEATURE_NOT_AVAILABLE);
232     }
233 
234     @Test
235     @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
getUnusedAppRestrictionsStatus_preApi30_noVerifierRevokeApp_returnsNotAvailable()236     public void getUnusedAppRestrictionsStatus_preApi30_noVerifierRevokeApp_returnsNotAvailable()
237             throws Exception {
238         setupPermissionRevocationApps(mPackageManager, Arrays.asList(NON_VERIFIER_PACKAGE_NAME));
239         // Do not set this app as the Verifier on the device
240         when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
241                 NON_VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_DENIED);
242 
243         ListenableFuture<Integer> resultFuture =
244                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
245 
246         assertThat(resultFuture.get()).isEqualTo(FEATURE_NOT_AVAILABLE);
247     }
248 
249     @Test
250     @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
getUnusedAppRestrictionsStatus_preApi30_verifierRevocationApp_bindsService()251     public void getUnusedAppRestrictionsStatus_preApi30_verifierRevocationApp_bindsService() {
252         ApplicationInfo appInfo = new ApplicationInfo();
253         // Mark the application as targeting API 30+
254         appInfo.targetSdkVersion = R;
255         when(mContext.getApplicationInfo()).thenReturn(appInfo);
256         setupPermissionRevocationApps(mPackageManager, Arrays.asList(VERIFIER_PACKAGE_NAME));
257         // Set this app as the Verifier on the device
258         when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
259                 VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_GRANTED);
260 
261         PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
262 
263         verify(mContext).bindService(
264                 mIntentCaptor.capture(),
265                 any(UnusedAppRestrictionsBackportServiceConnection.class),
266                 eq(Context.BIND_AUTO_CREATE));
267         Intent actualIntent = mIntentCaptor.getValue();
268         assertThat(actualIntent.getPackage()).isEqualTo(VERIFIER_PACKAGE_NAME);
269         assertThat(actualIntent.getAction())
270                 .isEqualTo(ACTION_UNUSED_APP_RESTRICTIONS_BACKPORT_CONNECTION);
271         // We do not check the future value as this would require constructing a fake service to
272         // connect to.
273     }
274 
275     @Test
276     @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
getUnusedAppRestrictionsStatus_preApi30_manyVerifierRevocationApps_doesNotThrow()277     public void getUnusedAppRestrictionsStatus_preApi30_manyVerifierRevocationApps_doesNotThrow() {
278         ApplicationInfo appInfo = new ApplicationInfo();
279         // Mark the application as targeting API 30+
280         appInfo.targetSdkVersion = R;
281         when(mContext.getApplicationInfo()).thenReturn(appInfo);
282         setupPermissionRevocationApps(mPackageManager, Arrays.asList(VERIFIER_PACKAGE_NAME,
283                 VERIFIER_PACKAGE_NAME2));
284         // Set both apps as the Verifier on the device, but we should have a graceful failure.
285         when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
286                 VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_GRANTED);
287         when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
288                 VERIFIER_PACKAGE_NAME2)).thenReturn(PERMISSION_GRANTED);
289 
290         PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
291 
292         verify(mContext).bindService(
293                 mIntentCaptor.capture(),
294                 any(UnusedAppRestrictionsBackportServiceConnection.class),
295                 eq(Context.BIND_AUTO_CREATE));
296         Intent actualIntent = mIntentCaptor.getValue();
297         assertThat(actualIntent.getPackage()).isEqualTo(VERIFIER_PACKAGE_NAME);
298         assertThat(actualIntent.getAction())
299                 .isEqualTo(ACTION_UNUSED_APP_RESTRICTIONS_BACKPORT_CONNECTION);
300         // We do not check the future value as this would require constructing a fake service to
301         // connect to.
302     }
303 
304     @Test
305     @SdkSuppress(maxSdkVersion = LOLLIPOP)
getUnusedAppRestrictionsStatus_preApi23_returnsFeatureNotAvailable()306     public void getUnusedAppRestrictionsStatus_preApi23_returnsFeatureNotAvailable()
307             throws Exception {
308         ListenableFuture<Integer> resultFuture =
309                 PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext);
310 
311         assertThat(resultFuture.get()).isEqualTo(FEATURE_NOT_AVAILABLE);
312     }
313 
314     /**
315      * Setup applications with the verifier role can handle unused app restriction features. In
316      * this case, they are permission revocation apps.
317      */
318     @SuppressWarnings("deprecation")
setupPermissionRevocationApps( PackageManager packageManager, List<String> packageNames)319     static void setupPermissionRevocationApps(
320             PackageManager packageManager, List<String> packageNames) {
321         List<ResolveInfo> resolveInfos = new ArrayList<>();
322 
323         for (String packageName : packageNames) {
324             ApplicationInfo appInfo = new ApplicationInfo();
325             appInfo.uid = 12345;
326             appInfo.packageName = packageName;
327 
328             ActivityInfo activityInfo = new ActivityInfo();
329             activityInfo.packageName = packageName;
330             activityInfo.name = "Name needed to keep toString() happy :)";
331             activityInfo.applicationInfo = appInfo;
332 
333             ResolveInfo resolveInfo = new ResolveInfo();
334             resolveInfo.activityInfo = activityInfo;
335             resolveInfo.providerInfo = new ProviderInfo();
336             resolveInfo.providerInfo.name = "Name needed to keep toString() happy :)";
337 
338             resolveInfos.add(resolveInfo);
339         }
340 
341         // Mark the applications as being able to resolve the AUTO_REVOKE_PERMISSIONS intent
342         when(packageManager.queryIntentActivities(
343                 nullable(Intent.class), eq(0))).thenReturn(resolveInfos);
344     }
345 }
346