| /* |
| * Copyright (C) 2018 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 com.android.phone; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyString; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.ArgumentMatchers.nullable; |
| import static org.mockito.Mockito.when; |
| |
| import android.Manifest; |
| import android.app.AppOpsManager; |
| import android.content.Context; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.location.LocationManager; |
| import android.os.Build; |
| import android.os.UserHandle; |
| import android.telephony.LocationAccessPolicy; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| |
| @RunWith(Parameterized.class) |
| public class LocationAccessPolicyTest { |
| private static class Scenario { |
| static class Builder { |
| private int mAppSdkLevel; |
| private boolean mAppHasFineManifest = false; |
| private boolean mAppHasCoarseManifest = false; |
| private int mFineAppOp = AppOpsManager.MODE_IGNORED; |
| private int mCoarseAppOp = AppOpsManager.MODE_IGNORED; |
| private boolean mIsDynamicLocationEnabled; |
| private LocationAccessPolicy.LocationPermissionQuery mQuery; |
| private LocationAccessPolicy.LocationPermissionResult mExpectedResult; |
| private String mName; |
| |
| public Builder setAppSdkLevel(int appSdkLevel) { |
| mAppSdkLevel = appSdkLevel; |
| return this; |
| } |
| |
| public Builder setAppHasFineManifest(boolean appHasFineManifest) { |
| mAppHasFineManifest = appHasFineManifest; |
| return this; |
| } |
| |
| public Builder setAppHasCoarseManifest( |
| boolean appHasCoarseManifest) { |
| mAppHasCoarseManifest = appHasCoarseManifest; |
| return this; |
| } |
| |
| public Builder setFineAppOp(int fineAppOp) { |
| mFineAppOp = fineAppOp; |
| return this; |
| } |
| |
| public Builder setCoarseAppOp(int coarseAppOp) { |
| mCoarseAppOp = coarseAppOp; |
| return this; |
| } |
| |
| public Builder setIsDynamicLocationEnabled( |
| boolean isDynamicLocationEnabled) { |
| mIsDynamicLocationEnabled = isDynamicLocationEnabled; |
| return this; |
| } |
| |
| public Builder setQuery( |
| LocationAccessPolicy.LocationPermissionQuery query) { |
| mQuery = query; |
| return this; |
| } |
| |
| public Builder setExpectedResult( |
| LocationAccessPolicy.LocationPermissionResult expectedResult) { |
| mExpectedResult = expectedResult; |
| return this; |
| } |
| |
| public Builder setName(String name) { |
| mName = name; |
| return this; |
| } |
| |
| public Scenario build() { |
| return new Scenario(mAppSdkLevel, mAppHasFineManifest, mAppHasCoarseManifest, |
| mFineAppOp, mCoarseAppOp, mIsDynamicLocationEnabled, mQuery, |
| mExpectedResult, mName); |
| } |
| } |
| int appSdkLevel; |
| boolean appHasFineManifest; |
| boolean appHasCoarseManifest; |
| int fineAppOp; |
| int coarseAppOp; |
| boolean isDynamicLocationEnabled; |
| LocationAccessPolicy.LocationPermissionQuery query; |
| LocationAccessPolicy.LocationPermissionResult expectedResult; |
| String name; |
| |
| private Scenario(int appSdkLevel, boolean appHasFineManifest, boolean appHasCoarseManifest, |
| int fineAppOp, int coarseAppOp, |
| boolean isDynamicLocationEnabled, |
| LocationAccessPolicy.LocationPermissionQuery query, |
| LocationAccessPolicy.LocationPermissionResult expectedResult, |
| String name) { |
| this.appSdkLevel = appSdkLevel; |
| this.appHasFineManifest = appHasFineManifest; |
| this.appHasCoarseManifest = appHasFineManifest || appHasCoarseManifest; |
| this.fineAppOp = fineAppOp; |
| this.coarseAppOp = coarseAppOp == AppOpsManager.MODE_ALLOWED ? coarseAppOp : fineAppOp; |
| this.isDynamicLocationEnabled = isDynamicLocationEnabled; |
| this.query = query; |
| this.expectedResult = expectedResult; |
| this.name = name; |
| } |
| |
| @Override |
| public String toString() { |
| return name; |
| } |
| } |
| |
| private static final int TESTING_UID = 10001; |
| private static final int TESTING_PID = 8009; |
| private static final String TESTING_CALLING_PACKAGE = "com.android.phone"; |
| |
| @Mock Context mContext; |
| @Mock AppOpsManager mAppOpsManager; |
| @Mock LocationManager mLocationManager; |
| @Mock PackageManager mPackageManager; |
| @Mock Resources mResources; |
| Scenario mScenario; |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| mockContextSystemService(AppOpsManager.class, mAppOpsManager); |
| mockContextSystemService(LocationManager.class, mLocationManager); |
| mockContextSystemService(PackageManager.class, mPackageManager); |
| when(mContext.getPackageManager()).thenReturn(mPackageManager); |
| when(mContext.getResources()).thenReturn(mResources); |
| when(mResources.getStringArray( |
| com.android.internal.R.array.config_serviceStateLocationAllowedPackages)) |
| .thenReturn(new String[]{TESTING_CALLING_PACKAGE}); |
| } |
| |
| private <T> void mockContextSystemService(Class<T> clazz , T obj) { |
| when(mContext.getSystemServiceName(eq(clazz))).thenReturn(clazz.getSimpleName()); |
| when(mContext.getSystemService(clazz.getSimpleName())).thenReturn(obj); |
| } |
| |
| public LocationAccessPolicyTest(Scenario scenario) { |
| mScenario = scenario; |
| } |
| |
| |
| @Test |
| public void test() { |
| setupScenario(mScenario); |
| assertEquals(mScenario.expectedResult, |
| LocationAccessPolicy.checkLocationPermission(mContext, mScenario.query)); |
| } |
| |
| private void setupScenario(Scenario s) { |
| when(mContext.checkPermission(eq(Manifest.permission.ACCESS_FINE_LOCATION), |
| anyInt(), anyInt())).thenReturn(s.appHasFineManifest |
| ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED); |
| |
| when(mContext.checkPermission(eq(Manifest.permission.ACCESS_COARSE_LOCATION), |
| anyInt(), anyInt())).thenReturn(s.appHasCoarseManifest |
| ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED); |
| |
| when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_FINE_LOCATION), |
| anyInt(), anyString(), nullable(String.class), nullable(String.class))) |
| .thenReturn(s.fineAppOp); |
| when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_COARSE_LOCATION), |
| anyInt(), anyString(), nullable(String.class), nullable(String.class))) |
| .thenReturn(s.coarseAppOp); |
| |
| // set this permission to denied by default, and only allow for the proper pid/uid |
| // combination |
| when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL), |
| anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED); |
| if (s.isDynamicLocationEnabled) { |
| when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class))).thenReturn(true); |
| when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL), |
| eq(TESTING_PID), eq(TESTING_UID))) |
| .thenReturn(PackageManager.PERMISSION_GRANTED); |
| } else { |
| when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class))) |
| .thenReturn(false); |
| } |
| |
| ApplicationInfo fakeAppInfo = new ApplicationInfo(); |
| fakeAppInfo.targetSdkVersion = s.appSdkLevel; |
| |
| try { |
| when(mPackageManager.getApplicationInfo(anyString(), anyInt())) |
| .thenReturn(fakeAppInfo); |
| } catch (Exception e) { |
| // this is a formality |
| } |
| } |
| |
| private static LocationAccessPolicy.LocationPermissionQuery.Builder getDefaultQueryBuilder() { |
| return new LocationAccessPolicy.LocationPermissionQuery.Builder() |
| .setMethod("test") |
| .setCallingPackage("com.android.test") |
| .setCallingFeatureId(null) |
| .setCallingPid(TESTING_PID) |
| .setCallingUid(TESTING_UID); |
| } |
| |
| @Parameterized.Parameters(name = "{0}") |
| public static Collection<Scenario> getScenarios() { |
| List<Scenario> scenarios = new ArrayList<>(); |
| scenarios.add(new Scenario.Builder() |
| .setName("System location is off") |
| .setAppHasFineManifest(true) |
| .setFineAppOp(AppOpsManager.MODE_ALLOWED) |
| .setAppSdkLevel(Build.VERSION_CODES.P) |
| .setIsDynamicLocationEnabled(false) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.N) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_SOFT) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("System location is off but package is allowlisted for location") |
| .setAppHasFineManifest(true) |
| .setFineAppOp(AppOpsManager.MODE_ALLOWED) |
| .setAppSdkLevel(Build.VERSION_CODES.P) |
| .setIsDynamicLocationEnabled(false) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.N) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.N) |
| .setCallingPackage(TESTING_CALLING_PACKAGE).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App on latest SDK level has all proper permissions for fine") |
| .setAppHasFineManifest(true) |
| .setFineAppOp(AppOpsManager.MODE_ALLOWED) |
| .setAppSdkLevel(Build.VERSION_CODES.P) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.N) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App on older SDK level missing permissions for fine but has coarse") |
| .setAppHasCoarseManifest(true) |
| .setCoarseAppOp(AppOpsManager.MODE_ALLOWED) |
| .setAppSdkLevel(Build.VERSION_CODES.JELLY_BEAN) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.JELLY_BEAN) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.M) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.JELLY_BEAN).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App on latest SDK level missing fine app ops permission") |
| .setAppHasFineManifest(true) |
| .setFineAppOp(AppOpsManager.MODE_ERRORED) |
| .setAppSdkLevel(Build.VERSION_CODES.P) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.N) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App has coarse permission but fine permission isn't being enforced yet") |
| .setAppHasCoarseManifest(true) |
| .setCoarseAppOp(AppOpsManager.MODE_ALLOWED) |
| .setAppSdkLevel(LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForFine( |
| LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1) |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App on latest SDK level has coarse but missing fine when fine is req.") |
| .setAppHasCoarseManifest(true) |
| .setCoarseAppOp(AppOpsManager.MODE_ALLOWED) |
| .setAppSdkLevel(Build.VERSION_CODES.P) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.P) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App on latest SDK level has MODE_IGNORED for app ops on fine") |
| .setAppHasCoarseManifest(true) |
| .setCoarseAppOp(AppOpsManager.MODE_ALLOWED) |
| .setFineAppOp(AppOpsManager.MODE_IGNORED) |
| .setAppSdkLevel(Build.VERSION_CODES.P) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.O) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.P) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App has no permissions but it's sdk level grandfathers it in") |
| .setAppSdkLevel(Build.VERSION_CODES.N) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.O) |
| .setMinSdkVersionForFine(Build.VERSION_CODES.Q) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED) |
| .build()); |
| |
| scenarios.add(new Scenario.Builder() |
| .setName("App on latest SDK level has proper permissions for coarse") |
| .setAppHasCoarseManifest(true) |
| .setCoarseAppOp(AppOpsManager.MODE_ALLOWED) |
| .setAppSdkLevel(Build.VERSION_CODES.P) |
| .setIsDynamicLocationEnabled(true) |
| .setQuery(getDefaultQueryBuilder() |
| .setMinSdkVersionForEnforcement(Build.VERSION_CODES.P) |
| .setMinSdkVersionForFine( |
| LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1) |
| .setMinSdkVersionForCoarse(Build.VERSION_CODES.P).build()) |
| .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED) |
| .build()); |
| return scenarios; |
| } |
| } |