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