1 /*
2  * Copyright (C) 2020 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 com.android.server.appop;
18 
19 import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM;
20 import static android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT;
21 import static android.app.AppOpsManager._NUM_OP;
22 
23 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
24 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
25 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
26 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
27 
28 import static org.junit.Assert.assertEquals;
29 import static org.junit.Assert.assertFalse;
30 import static org.junit.Assert.assertTrue;
31 import static org.junit.Assert.fail;
32 import static org.mockito.ArgumentMatchers.anyInt;
33 import static org.mockito.ArgumentMatchers.anyLong;
34 import static org.mockito.ArgumentMatchers.anyString;
35 import static org.mockito.ArgumentMatchers.isA;
36 import static org.mockito.ArgumentMatchers.nullable;
37 import static org.mockito.Mockito.doNothing;
38 import static org.mockito.Mockito.never;
39 import static org.mockito.Mockito.spy;
40 
41 import android.app.AppOpsManager;
42 import android.content.Context;
43 import android.content.pm.PackageManager;
44 import android.content.pm.PackageManagerInternal;
45 import android.content.pm.UserPackage;
46 import android.content.res.AssetManager;
47 import android.os.Handler;
48 import android.os.UserHandle;
49 import android.permission.PermissionManager;
50 import android.util.ArrayMap;
51 import android.util.Log;
52 import android.util.SparseArray;
53 import android.util.Xml;
54 
55 import androidx.test.InstrumentationRegistry;
56 import androidx.test.filters.SmallTest;
57 import androidx.test.runner.AndroidJUnit4;
58 
59 import com.android.internal.util.ArrayUtils;
60 import com.android.modules.utils.TypedXmlPullParser;
61 import com.android.server.LocalServices;
62 import com.android.server.SystemServerInitThreadPool;
63 import com.android.server.pm.UserManagerInternal;
64 import com.android.server.pm.permission.PermissionManagerServiceInternal;
65 import com.android.server.pm.pkg.PackageStateInternal;
66 
67 import org.junit.After;
68 import org.junit.Before;
69 import org.junit.Test;
70 import org.junit.runner.RunWith;
71 import org.mockito.Mock;
72 import org.mockito.MockitoSession;
73 import org.mockito.quality.Strictness;
74 import org.xmlpull.v1.XmlPullParser;
75 
76 import java.io.File;
77 import java.io.FileInputStream;
78 import java.io.FileOutputStream;
79 import java.io.IOException;
80 import java.io.InputStream;
81 
82 /**
83  * Tests app ops version upgrades
84  */
85 @SmallTest
86 @RunWith(AndroidJUnit4.class)
87 public class AppOpsUpgradeTest {
88     private static final String TAG = AppOpsUpgradeTest.class.getSimpleName();
89     private static final String APP_OPS_UNVERSIONED_ASSET_PATH =
90             "AppOpsUpgradeTest/appops-unversioned.xml";
91     private static final String APP_OPS_VERSION_1_ASSET_PATH =
92             "AppOpsUpgradeTest/appops-version-1.xml";
93 
94     private static final String APP_OPS_VERSION_3_ASSET_PATH =
95             "AppOpsUpgradeTest/appops-version-3.xml";
96 
97     private static final String APP_OPS_FILENAME = "appops-test.xml";
98 
99     private static final Context sContext = InstrumentationRegistry.getTargetContext();
100     private static final File sAppOpsFile = new File(sContext.getFilesDir(), APP_OPS_FILENAME);
101 
102     private Context mTestContext;
103     private MockitoSession mMockitoSession;
104 
105     @Mock
106     private PackageManagerInternal mPackageManagerInternal;
107     @Mock
108     private PackageManager mPackageManager;
109     @Mock
110     private UserManagerInternal mUserManagerInternal;
111     @Mock
112     private PermissionManagerServiceInternal mPermissionManagerInternal;
113     @Mock
114     private Handler mHandler;
115     @Mock
116     private PermissionManager mPermissionManager;
117 
118     private Object mLock = new Object();
119     private SparseArray<int[]> mSwitchedOps;
120 
extractAppOpsFile(String assetPath)121     private static void extractAppOpsFile(String assetPath) {
122         sAppOpsFile.getParentFile().mkdirs();
123         try (FileOutputStream out = new FileOutputStream(sAppOpsFile);
124              InputStream in = sContext.getAssets().open(assetPath, AssetManager.ACCESS_BUFFER)) {
125             byte[] buffer = new byte[4096];
126             int bytesRead;
127             while ((bytesRead = in.read(buffer)) >= 0) {
128                 out.write(buffer, 0, bytesRead);
129             }
130             out.flush();
131             Log.d(TAG, "Successfully copied xml to " + sAppOpsFile.getAbsolutePath());
132         } catch (IOException exc) {
133             Log.e(TAG, "Exception while copying appops xml", exc);
134             fail();
135         }
136     }
137 
138 
139     @Before
setUp()140     public void setUp() {
141         if (sAppOpsFile.exists()) {
142             sAppOpsFile.delete();
143         }
144 
145         mMockitoSession = mockitoSession()
146                 .initMocks(this)
147                 .spyStatic(LocalServices.class)
148                 .mockStatic(SystemServerInitThreadPool.class)
149                 .strictness(Strictness.LENIENT)
150                 .startMocking();
151 
152         doReturn(mPermissionManagerInternal).when(
153                 () -> LocalServices.getService(PermissionManagerServiceInternal.class));
154         doReturn(mUserManagerInternal).when(
155                 () -> LocalServices.getService(UserManagerInternal.class));
156         doReturn(mPackageManagerInternal).when(
157                 () -> LocalServices.getService(PackageManagerInternal.class));
158 
159         mTestContext = spy(sContext);
160 
161         // Pretend everybody has all permissions
162         doNothing().when(mTestContext).enforcePermission(anyString(), anyInt(), anyInt(),
163                 nullable(String.class));
164 
165         doReturn(mPackageManager).when(mTestContext).getPackageManager();
166 
167         // Stub out package calls to disable AppOpsService#updatePermissionRevokedCompat
168         doReturn(null).when(mPackageManager).getPackagesForUid(anyInt());
169 
170         doReturn(new ArrayMap<String, PackageStateInternal>()).when(mPackageManagerInternal)
171                 .getPackageStates();
172 
173         doReturn(new int[] {0}).when(mUserManagerInternal).getUserIds();
174 
175         // Build mSwitchedOps
176         mSwitchedOps = buildSwitchedOpsArray();
177     }
178 
buildSwitchedOpsArray()179     private SparseArray<int[]> buildSwitchedOpsArray() {
180         SparseArray<int[]> switchedOps = new SparseArray<>();
181         for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) {
182             int switchCode = AppOpsManager.opToSwitch(switchedCode);
183             switchedOps.put(switchCode,
184                     ArrayUtils.appendInt(switchedOps.get(switchCode), switchedCode));
185         }
186         return switchedOps;
187     }
188 
189     @After
tearDown()190     public void tearDown() {
191         mMockitoSession.finishMocking();
192     }
193 
194     @Test
upgradeRunAnyInBackground()195     public void upgradeRunAnyInBackground() {
196         extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH);
197 
198         AppOpsCheckingServiceImpl testService = new AppOpsCheckingServiceImpl(sAppOpsFile, mLock,
199                 mHandler, mTestContext, mSwitchedOps);
200         testService.readState();
201 
202         testService.upgradeRunAnyInBackgroundLocked();
203 
204         assertSameModes(testService, AppOpsManager.OP_RUN_IN_BACKGROUND,
205                 AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
206     }
207 
assertSameModes(AppOpsCheckingServiceImpl testService, int op1, int op2)208     private void assertSameModes(AppOpsCheckingServiceImpl testService, int op1, int op2) {
209         for (int uid : testService.getUidsWithNonDefaultModes()) {
210             assertEquals(
211                     testService.getUidMode(uid, op1),
212                     testService.getUidMode(uid, op2)
213             );
214         }
215         for (UserPackage pkg : testService.getPackagesWithNonDefaultModes()) {
216             assertEquals(
217                     testService.getPackageMode(pkg.packageName, op1, pkg.userId),
218                     testService.getPackageMode(pkg.packageName, op2, pkg.userId)
219             );
220         }
221     }
222 
getModeInFile(int uid, int op)223     private static int getModeInFile(int uid, int op) {
224         switch (uid) {
225             case 10198:
226                 return 0;
227             case 10200:
228                 return 1;
229             case 1110200:
230             case 10267:
231             case 1110181:
232                 return 2;
233             default:
234                 return AppOpsManager.opToDefaultMode(op);
235         }
236     }
237 
238     @Test
upgradeScheduleExactAlarm()239     public void upgradeScheduleExactAlarm() {
240         extractAppOpsFile(APP_OPS_VERSION_1_ASSET_PATH);
241 
242         String[] packageNames = {"p1", "package2", "pkg3", "package.4", "pkg-5", "pkg.6"};
243         int[] appIds = {10267, 10181, 10198, 10199, 10200, 4213};
244         int[] userIds = {0, 10, 11};
245 
246         doReturn(userIds).when(mUserManagerInternal).getUserIds();
247 
248         doReturn(packageNames).when(mPermissionManagerInternal).getAppOpPermissionPackages(
249                 AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM));
250 
251         doAnswer(invocation -> {
252             String pkg = invocation.getArgument(0);
253             int index = ArrayUtils.indexOf(packageNames, pkg);
254             if (index < 0) {
255                 return index;
256             }
257             int userId = invocation.getArgument(2);
258             return UserHandle.getUid(userId, appIds[index]);
259         }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
260 
261         AppOpsCheckingServiceImpl testService = new AppOpsCheckingServiceImpl(sAppOpsFile, mLock,
262                 mHandler, mTestContext, mSwitchedOps);
263         testService.readState();
264 
265         testService.upgradeScheduleExactAlarmLocked();
266 
267         for (int userId : userIds) {
268             for (int appId : appIds) {
269                 final int uid = UserHandle.getUid(userId, appId);
270                 final int previousMode = getModeInFile(uid, OP_SCHEDULE_EXACT_ALARM);
271 
272                 final int expectedMode;
273                 if (previousMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) {
274                     expectedMode = AppOpsManager.MODE_ALLOWED;
275                 } else {
276                     expectedMode = previousMode;
277                 }
278                 int mode = testService.getUidMode(uid, OP_SCHEDULE_EXACT_ALARM);
279                 assertEquals(expectedMode, mode);
280             }
281         }
282 
283         // These uids don't even declare the permission. So should stay as default / empty.
284         int[] unrelatedUidsInFile = {10225, 10178};
285 
286         for (int uid : unrelatedUidsInFile) {
287             int mode = testService.getUidMode(uid, OP_SCHEDULE_EXACT_ALARM);
288             assertEquals(AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM), mode);
289         }
290     }
291 
292     @Test
resetUseFullScreenIntent()293     public void resetUseFullScreenIntent() {
294         extractAppOpsFile(APP_OPS_VERSION_3_ASSET_PATH);
295 
296         String[] packageNames = {"p1", "package2", "pkg3", "package.4", "pkg-5", "pkg.6"};
297         int[] appIds = {10267, 10181, 10198, 10199, 10200, 4213};
298         int[] userIds = {0, 10, 11};
299         int flag = 0;
300 
301         doReturn(userIds).when(mUserManagerInternal).getUserIds();
302 
303         doReturn(packageNames).when(mPermissionManagerInternal).getAppOpPermissionPackages(
304                 AppOpsManager.opToPermission(OP_USE_FULL_SCREEN_INTENT));
305 
306         doReturn(mPermissionManager).when(mTestContext).getSystemService(PermissionManager.class);
307 
308         doReturn(flag).when(mPackageManager).getPermissionFlags(
309                 anyString(), anyString(), isA(UserHandle.class));
310 
311         doAnswer(invocation -> {
312             String pkg = invocation.getArgument(0);
313             int index = ArrayUtils.indexOf(packageNames, pkg);
314             if (index < 0) {
315                 return index;
316             }
317             int userId = invocation.getArgument(2);
318             return UserHandle.getUid(userId, appIds[index]);
319         }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
320 
321         AppOpsCheckingServiceImpl testService = new AppOpsCheckingServiceImpl(sAppOpsFile, mLock,
322                 mHandler, mTestContext, mSwitchedOps);
323         testService.readState();
324 
325         synchronized (testService) {
326             testService.resetUseFullScreenIntentLocked();
327         }
328 
329         for (int userId : userIds) {
330             for (int appId : appIds) {
331                 final int uid = UserHandle.getUid(userId, appId);
332                 final int expectedMode = AppOpsManager.opToDefaultMode(OP_USE_FULL_SCREEN_INTENT);
333                 synchronized (testService) {
334                     int mode = testService.getUidMode(uid, OP_USE_FULL_SCREEN_INTENT);
335                     assertEquals(expectedMode, mode);
336                 }
337             }
338         }
339     }
340 
341     @Test
upgradeFromNoFile()342     public void upgradeFromNoFile() {
343         assertFalse(sAppOpsFile.exists());
344 
345         AppOpsCheckingServiceImpl testService = spy(new AppOpsCheckingServiceImpl(sAppOpsFile,
346                 mLock, mHandler, mTestContext, mSwitchedOps));
347         testService.readState();
348 
349         doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
350         doNothing().when(testService).upgradeScheduleExactAlarmLocked();
351         doNothing().when(testService).resetUseFullScreenIntentLocked();
352 
353         // trigger upgrade
354         testService.systemReady();
355 
356         verify(testService, never()).upgradeRunAnyInBackgroundLocked();
357         verify(testService, never()).upgradeScheduleExactAlarmLocked();
358         verify(testService, never()).resetUseFullScreenIntentLocked();
359 
360         testService.writeState();
361 
362         assertTrue(sAppOpsFile.exists());
363 
364         AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
365         assertTrue(parser.parse());
366         assertEquals(AppOpsCheckingServiceImpl.CURRENT_VERSION, parser.mVersion);
367     }
368 
369     @Test
upgradeFromNoVersion()370     public void upgradeFromNoVersion() {
371         extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH);
372         AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
373         assertTrue(parser.parse());
374         assertEquals(AppOpsDataParser.NO_VERSION, parser.mVersion);
375 
376         AppOpsCheckingServiceImpl testService = spy(new AppOpsCheckingServiceImpl(sAppOpsFile,
377                 mLock, mHandler, mTestContext, mSwitchedOps));
378         testService.readState();
379 
380         doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
381         doNothing().when(testService).upgradeScheduleExactAlarmLocked();
382         doNothing().when(testService).resetUseFullScreenIntentLocked();
383 
384         // trigger upgrade
385         testService.systemReady();
386 
387         verify(testService).upgradeRunAnyInBackgroundLocked();
388         verify(testService).upgradeScheduleExactAlarmLocked();
389         verify(testService).resetUseFullScreenIntentLocked();
390 
391         testService.writeState();
392         assertTrue(parser.parse());
393         assertEquals(AppOpsCheckingServiceImpl.CURRENT_VERSION, parser.mVersion);
394     }
395 
396     @Test
upgradeFromVersion1()397     public void upgradeFromVersion1() {
398         extractAppOpsFile(APP_OPS_VERSION_1_ASSET_PATH);
399         AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
400         assertTrue(parser.parse());
401         assertEquals(1, parser.mVersion);
402 
403         AppOpsCheckingServiceImpl testService = spy(new AppOpsCheckingServiceImpl(sAppOpsFile,
404                 mLock, mHandler, mTestContext, mSwitchedOps));
405         testService.readState();
406 
407         doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
408         doNothing().when(testService).upgradeScheduleExactAlarmLocked();
409         doNothing().when(testService).resetUseFullScreenIntentLocked();
410 
411         // trigger upgrade
412         testService.systemReady();
413 
414         verify(testService, never()).upgradeRunAnyInBackgroundLocked();
415         verify(testService).upgradeScheduleExactAlarmLocked();
416         verify(testService).resetUseFullScreenIntentLocked();
417 
418         testService.writeState();
419         assertTrue(parser.parse());
420         assertEquals(AppOpsCheckingServiceImpl.CURRENT_VERSION, parser.mVersion);
421     }
422 
423     @Test
resetFromVersion3()424     public void resetFromVersion3() {
425         extractAppOpsFile(APP_OPS_VERSION_3_ASSET_PATH);
426         AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
427         assertTrue(parser.parse());
428         assertEquals(3, parser.mVersion);
429 
430         AppOpsCheckingServiceImpl testService = spy(new AppOpsCheckingServiceImpl(sAppOpsFile,
431                 mLock, mHandler, mTestContext, mSwitchedOps));
432         testService.readState();
433 
434         doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
435         doNothing().when(testService).upgradeScheduleExactAlarmLocked();
436         doNothing().when(testService).resetUseFullScreenIntentLocked();
437 
438         testService.systemReady();
439 
440         verify(testService, never()).upgradeRunAnyInBackgroundLocked();
441         verify(testService, never()).upgradeScheduleExactAlarmLocked();
442         verify(testService).resetUseFullScreenIntentLocked();
443 
444         testService.writeState();
445         assertTrue(parser.parse());
446         assertEquals(AppOpsCheckingServiceImpl.CURRENT_VERSION, parser.mVersion);
447     }
448 
449     /**
450      * Class to parse data from the appops xml. Currently only parses and holds the version number.
451      * Other fields may be added as and when required for testing.
452      */
453     private static final class AppOpsDataParser {
454         static final int NO_VERSION = -123;
455         int mVersion;
456         private File mFile;
457 
AppOpsDataParser(File file)458         AppOpsDataParser(File file) {
459             mFile = file;
460             mVersion = NO_VERSION;
461         }
462 
parse()463         boolean parse() {
464             try (FileInputStream stream = new FileInputStream(mFile)) {
465                 TypedXmlPullParser parser = Xml.resolvePullParser(stream);
466                 int type;
467                 while ((type = parser.next()) != XmlPullParser.START_TAG
468                         && type != XmlPullParser.END_DOCUMENT) {
469                     ;
470                 }
471                 if (type != XmlPullParser.START_TAG) {
472                     throw new IllegalStateException("no start tag found");
473                 }
474                 final String versionString = parser.getAttributeValue(null, "v");
475                 if (versionString != null) {
476                     mVersion = Integer.parseInt(versionString);
477                 }
478             } catch (Exception e) {
479                 Log.e(TAG, "Failed while parsing test appops xml", e);
480                 return false;
481             }
482             return true;
483         }
484     }
485 }
486