• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.anyInt;
22 import static org.mockito.Mockito.doReturn;
23 import static org.mockito.Mockito.mock;
24 import static org.mockito.Mockito.spy;
25 
26 import android.app.ActivityManagerInternal;
27 import android.app.pinner.PinnedFileStat;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.os.Binder;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.platform.test.annotations.DisableFlags;
35 import android.platform.test.annotations.EnableFlags;
36 import android.platform.test.flag.junit.SetFlagsRule;
37 import android.provider.DeviceConfig;
38 import android.provider.DeviceConfigInterface;
39 import android.testing.TestableContext;
40 import android.testing.TestableLooper;
41 import android.testing.TestableResources;
42 import android.util.ArrayMap;
43 import android.util.ArraySet;
44 
45 import androidx.test.InstrumentationRegistry;
46 import androidx.test.filters.SmallTest;
47 import androidx.test.runner.AndroidJUnit4;
48 
49 import com.android.server.flags.Flags;
50 import com.android.server.pinner.PinnedFile;
51 import com.android.server.pinner.PinnerService;
52 import com.android.server.testutils.FakeDeviceConfigInterface;
53 import com.android.server.wm.ActivityTaskManagerInternal;
54 
55 import org.junit.After;
56 import org.junit.Before;
57 import org.junit.Rule;
58 import org.junit.Test;
59 import org.junit.runner.RunWith;
60 import org.mockito.Mockito;
61 import org.mockito.MockitoAnnotations;
62 
63 import java.io.CharArrayWriter;
64 import java.io.FileDescriptor;
65 import java.io.PrintWriter;
66 import java.lang.reflect.Constructor;
67 import java.lang.reflect.Field;
68 import java.lang.reflect.Method;
69 import java.util.List;
70 import java.util.concurrent.TimeUnit;
71 
72 @SmallTest
73 @RunWith(AndroidJUnit4.class)
74 @TestableLooper.RunWithLooper
75 public class PinnerServiceTest {
76     private static final int KEY_CAMERA = 0;
77     private static final int KEY_HOME = 1;
78     private static final int KEY_ASSISTANT = 2;
79 
80     private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
81 
82     private static final int MEMORY_PERCENTAGE_FOR_QUOTA = 10;
83 
84     @Rule
85     public TestableContext mContext =
86             new TestableContext(InstrumentationRegistry.getContext(), null);
87 
88     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
89 
90     private final ArraySet<String> mUpdatedPackages = new ArraySet<>();
91     private ResolveInfo mHomePackageResolveInfo;
92     private FakeDeviceConfigInterface mFakeDeviceConfigInterface;
93     private PinnerService.Injector mInjector;
94     @Before
setUp()95     public void setUp() {
96         MockitoAnnotations.initMocks(this);
97 
98         if (Looper.myLooper() == null) {
99             Looper.prepare();
100         }
101 
102         // PinnerService.onStart will add itself as a local service, remove to avoid conflicts.
103         LocalServices.removeServiceForTest(PinnerService.class);
104         LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
105         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
106 
107         ActivityTaskManagerInternal mockActivityTaskManagerInternal = mock(
108                 ActivityTaskManagerInternal.class);
109         Intent homeIntent = getHomeIntent();
110 
111         doReturn(homeIntent).when(mockActivityTaskManagerInternal).getHomeIntent();
112         LocalServices.addService(ActivityTaskManagerInternal.class,
113                 mockActivityTaskManagerInternal);
114 
115         ActivityManagerInternal mockActivityManagerInternal = mock(ActivityManagerInternal.class);
116         doReturn(true).when(mockActivityManagerInternal).isUidActive(anyInt());
117         LocalServices.addService(ActivityManagerInternal.class, mockActivityManagerInternal);
118 
119         // Configure the default state to disable any pinning.
120         TestableResources resources = mContext.getOrCreateTestableResources();
121         resources.addOverride(
122                 com.android.internal.R.array.config_defaultPinnerServiceFiles, new String[0]);
123         resources.addOverride(com.android.internal.R.bool.config_pinnerCameraApp, false);
124         resources.addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 0);
125         resources.addOverride(com.android.internal.R.bool.config_pinnerAssistantApp, false);
126         resources.addOverride(com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage,
127                 MEMORY_PERCENTAGE_FOR_QUOTA);
128 
129         mFakeDeviceConfigInterface = new FakeDeviceConfigInterface();
130         setDeviceConfigPinnedAnonSize(0);
131 
132         mContext = spy(mContext);
133 
134         // Get HOME (Launcher) package
135         mHomePackageResolveInfo = mContext.getPackageManager().resolveActivityAsUser(homeIntent,
136                 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE
137                         | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
138         mUpdatedPackages.add(mHomePackageResolveInfo.activityInfo.applicationInfo.packageName);
139 
140         mInjector = new PinnerService.Injector() {
141             @Override
142             protected DeviceConfigInterface getDeviceConfigInterface() {
143                 return mFakeDeviceConfigInterface;
144             }
145 
146             @Override
147             protected void publishBinderService(PinnerService service, Binder binderService) {
148                 // Suppress this for testing, it's not needed and causes conflitcs.
149             }
150 
151             @Override
152             protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
153                     long maxBytesToPin, boolean attemptPinIntrospection) {
154                 return new PinnedFile(-1, maxBytesToPin, fileToPin, maxBytesToPin);
155             }
156         };
157     }
158 
159     @After
tearDown()160     public void tearDown() {
161         Mockito.framework().clearInlineMocks();
162     }
163 
getHomeIntent()164     private Intent getHomeIntent() {
165         Intent intent = new Intent(Intent.ACTION_MAIN);
166         intent.addCategory(Intent.CATEGORY_HOME);
167         intent.addCategory(Intent.CATEGORY_DEFAULT);
168         return intent;
169     }
170 
unpinAll(PinnerService pinnerService)171     private void unpinAll(PinnerService pinnerService) throws Exception {
172         Method unpinAppsMethod = PinnerService.class.getDeclaredMethod("unpinApps");
173         unpinAppsMethod.setAccessible(true);
174         unpinAppsMethod.invoke(pinnerService);
175         Method unpinAnonRegionMethod = PinnerService.class.getDeclaredMethod("unpinAnonRegion");
176         unpinAnonRegionMethod.setAccessible(true);
177         unpinAnonRegionMethod.invoke(pinnerService);
178     }
179 
getGlobalPinQuota(PinnerService service)180     private long getGlobalPinQuota(PinnerService service) throws Exception {
181         Method getQuotaMethod = PinnerService.class.getDeclaredMethod("getAvailableGlobalQuota");
182         getQuotaMethod.setAccessible(true);
183         return (long) getQuotaMethod.invoke(service);
184     }
185 
waitForPinnerService(PinnerService pinnerService)186     private void waitForPinnerService(PinnerService pinnerService)
187             throws NoSuchFieldException, IllegalAccessException {
188         // There's no notification/callback when pinning finished
189         // Block until pinner handler is done pinning and runs this empty runnable
190         Field pinnerHandlerField = PinnerService.class.getDeclaredField("mPinnerHandler");
191         pinnerHandlerField.setAccessible(true);
192         Handler pinnerServiceHandler = (Handler) pinnerHandlerField.get(pinnerService);
193         pinnerServiceHandler.runWithScissors(() -> {
194         }, WAIT_FOR_PINNER_TIMEOUT);
195     }
196 
getPinKeys(PinnerService pinnerService)197     private ArraySet<Integer> getPinKeys(PinnerService pinnerService)
198             throws NoSuchFieldException, IllegalAccessException {
199         Field pinKeysArrayField = PinnerService.class.getDeclaredField("mPinKeys");
200         pinKeysArrayField.setAccessible(true);
201         return (ArraySet<Integer>) pinKeysArrayField.get(pinnerService);
202     }
203 
getPinnedApps(PinnerService pinnerService)204     private ArrayMap<Integer, Object> getPinnedApps(PinnerService pinnerService)
205             throws NoSuchFieldException, IllegalAccessException {
206         Field pinnedAppsField = PinnerService.class.getDeclaredField("mPinnedApps");
207         pinnedAppsField.setAccessible(true);
208         return (ArrayMap<Integer, Object>) pinnedAppsField.get(
209                 pinnerService);
210     }
211 
getPinnerServiceDump(PinnerService pinnerService)212     private String getPinnerServiceDump(PinnerService pinnerService) throws Exception {
213         Class<?> innerClass = Class.forName(PinnerService.class.getName() + "$BinderService");
214         Constructor<?> ctor = innerClass.getDeclaredConstructor(PinnerService.class);
215         ctor.setAccessible(true);
216         Binder innerInstance = (Binder) ctor.newInstance(pinnerService);
217         CharArrayWriter cw = new CharArrayWriter();
218         PrintWriter pw = new PrintWriter(cw, true);
219         Method dumpMethod = Binder.class.getDeclaredMethod("dump", FileDescriptor.class,
220                 PrintWriter.class, String[].class);
221         dumpMethod.setAccessible(true);
222         dumpMethod.invoke(innerInstance, null, pw, null);
223         return cw.toString();
224     }
225 
getPinnedSize(PinnerService pinnerService)226     private long getPinnedSize(PinnerService pinnerService) {
227         long totalBytesPinned = 0;
228         for (PinnedFileStat stat : pinnerService.getPinnerStats()) {
229             totalBytesPinned += stat.getBytesPinned();
230         }
231         return totalBytesPinned;
232     }
233 
getPinnedAnonSize(PinnerService pinnerService)234     private int getPinnedAnonSize(PinnerService pinnerService) {
235         List<PinnedFileStat> anonStats = pinnerService.getPinnerStats().stream()
236                 .filter(pf -> pf.getGroupName().equals(PinnerService.ANON_REGION_STAT_NAME))
237                 .toList();
238         int totalAnon = 0;
239         for (PinnedFileStat anonStat : anonStats) {
240             totalAnon += anonStat.getBytesPinned();
241         }
242         return totalAnon;
243     }
244 
getTotalPinnedFiles(PinnerService pinnerService)245     private long getTotalPinnedFiles(PinnerService pinnerService) {
246         return pinnerService.getPinnerStats().stream().count();
247     }
248 
setDeviceConfigPinnedAnonSize(long size)249     private void setDeviceConfigPinnedAnonSize(long size) {
250         mFakeDeviceConfigInterface.setProperty(
251                 DeviceConfig.NAMESPACE_RUNTIME_NATIVE,
252                 "pin_shared_anon_size",
253                 String.valueOf(size),
254                 /*makeDefault=*/false);
255     }
256 
257     @Test
testPinHomeApp()258     public void testPinHomeApp() throws Exception {
259         // Enable HOME app pinning
260         mContext.getOrCreateTestableResources()
261                 .addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 1024);
262         PinnerService pinnerService = new PinnerService(mContext, mInjector);
263         pinnerService.onStart();
264 
265         ArraySet<Integer> pinKeys = getPinKeys(pinnerService);
266         assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME);
267 
268         pinnerService.update(mUpdatedPackages, true);
269 
270         waitForPinnerService(pinnerService);
271 
272         ArrayMap<Integer, Object> pinnedApps = getPinnedApps(pinnerService);
273         assertThat(pinnedApps.get(KEY_HOME)).isNotNull();
274 
275         assertThat(getPinnedSize(pinnerService)).isGreaterThan(0);
276         assertThat(getTotalPinnedFiles(pinnerService)).isGreaterThan(0);
277 
278         unpinAll(pinnerService);
279     }
280 
281     @Test
testPinHomeAppOnBootCompleted()282     public void testPinHomeAppOnBootCompleted() throws Exception {
283         // Enable HOME app pinning
284         mContext.getOrCreateTestableResources()
285                 .addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 1024);
286         PinnerService pinnerService = new PinnerService(mContext, mInjector);
287         pinnerService.onStart();
288 
289         ArraySet<Integer> pinKeys = getPinKeys(pinnerService);
290         assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME);
291 
292         pinnerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
293 
294         waitForPinnerService(pinnerService);
295 
296         ArrayMap<Integer, Object> pinnedApps = getPinnedApps(pinnerService);
297         assertThat(pinnedApps.get(KEY_HOME)).isNotNull();
298 
299         assertThat(getPinnedSize(pinnerService)).isGreaterThan(0);
300 
301         unpinAll(pinnerService);
302     }
303 
304     @Test
testNothingToPin()305     public void testNothingToPin() throws Exception {
306         // No package enabled for pinning
307         PinnerService pinnerService = new PinnerService(mContext, mInjector);
308         pinnerService.onStart();
309 
310         ArraySet<Integer> pinKeys = getPinKeys(pinnerService);
311         assertThat(pinKeys).isEmpty();
312 
313         pinnerService.update(mUpdatedPackages, true);
314 
315         waitForPinnerService(pinnerService);
316 
317         ArrayMap<Integer, Object> pinnedApps = getPinnedApps(pinnerService);
318         assertThat(pinnedApps).isEmpty();
319 
320         long totalPinnedSizeBytes = getPinnedSize(pinnerService);
321         assertThat(totalPinnedSizeBytes).isEqualTo(0);
322 
323         int pinnedAnonSizeBytes = getPinnedAnonSize(pinnerService);
324         assertThat(pinnedAnonSizeBytes).isEqualTo(0);
325 
326         unpinAll(pinnerService);
327     }
328 
329     @Test
testPinFile()330     public void testPinFile() throws Exception {
331         PinnerService pinnerService = new PinnerService(mContext, mInjector);
332         pinnerService.onStart();
333 
334         pinnerService.pinFile("test_file", 4096, null, "my_group", false);
335 
336         assertThat(getPinnedSize(pinnerService)).isEqualTo(4096);
337         assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(1);
338 
339         unpinAll(pinnerService);
340     }
341 
342     @Test
343     @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
testPinAllQuota()344     public void testPinAllQuota() throws Exception {
345         PinnerService pinnerService = new PinnerService(mContext, mInjector);
346         pinnerService.onStart();
347 
348         long quota = getGlobalPinQuota(pinnerService);
349 
350         pinnerService.pinFile("test_file", Long.MAX_VALUE, null, "my_group", false);
351 
352         assertThat(getPinnedSize(pinnerService)).isEqualTo(quota);
353 
354         unpinAll(pinnerService);
355     }
356 
357     @Test
358     @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
testGlobalPinQuotaAsDevicePercentage()359     public void testGlobalPinQuotaAsDevicePercentage() throws Exception {
360         PinnerService pinnerService = new PinnerService(mContext, mInjector);
361         pinnerService.onStart();
362         long origQuota = getGlobalPinQuota(pinnerService);
363 
364         long totalMem = android.os.Process.getTotalMemory();
365 
366         // Verify that pin quota is the set percentage of device total memory
367         assertThat(origQuota).isEqualTo((totalMem * MEMORY_PERCENTAGE_FOR_QUOTA) / 100);
368 
369         pinnerService.pinFile("test_file", 4096, null, "my_group", false);
370         assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota - 4096);
371     }
372 
373     @Test
374     @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
testGlobalPinWhenNoQuota()375     public void testGlobalPinWhenNoQuota() throws Exception {
376         TestableResources resources = mContext.getOrCreateTestableResources();
377         resources.addOverride(
378                 com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
379 
380         PinnerService pinnerService = new PinnerService(mContext, mInjector);
381         pinnerService.onStart();
382 
383         // Verify that pin quota is zero
384         assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
385 
386         pinnerService.pinFile("test_file", 4096, null, "my_group", false);
387         assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(0);
388     }
389 
390     /**
391      * This test is temporary, it should be cleaned up when removing the pin_global_quota bugfix
392      * flag.
393      */
394     @Test
395     @DisableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
testGlobalQuotaDisabled()396     public void testGlobalQuotaDisabled() throws Exception {
397         TestableResources resources = mContext.getOrCreateTestableResources();
398         resources.addOverride(
399                 com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
400 
401         PinnerService pinnerService = new PinnerService(mContext, mInjector);
402         pinnerService.onStart();
403 
404         // The quota parameter exists but it should have no effect on pinning
405         long quota = getGlobalPinQuota(pinnerService);
406 
407         pinnerService.pinFile("test_file", quota + 1, null, "my_group", false);
408 
409         // Verify that we can pin past the quota as it is disabled
410         assertThat(getPinnedSize(pinnerService)).isEqualTo(quota + 1);
411     }
412 
413     @Test
414     @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
testUnpinReleasesQuota()415     public void testUnpinReleasesQuota() throws Exception {
416         PinnerService pinnerService = new PinnerService(mContext, mInjector);
417         pinnerService.onStart();
418         long origQuota = getGlobalPinQuota(pinnerService);
419 
420         // Verify that pin quota exists and is non zero.
421         assertThat(getGlobalPinQuota(pinnerService)).isGreaterThan(0);
422 
423         pinnerService.pinFile("test_file", origQuota, null, "my_group", false);
424 
425         // Make sure all the quota was consumed
426         assertThat(getPinnedSize(pinnerService)).isEqualTo(origQuota);
427 
428         // Unpin the file and verify that the quota has been released.
429         pinnerService.unpinFile("test_file");
430         assertThat(getPinnedSize(pinnerService)).isEqualTo(0);
431         assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota);
432     }
433 
434     @Test
435     @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
testGlobalPinQuotaNegative()436     public void testGlobalPinQuotaNegative() throws Exception {
437         TestableResources resources = mContext.getOrCreateTestableResources();
438         resources.addOverride(
439                 com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, -10);
440 
441         PinnerService pinnerService = new PinnerService(mContext, mInjector);
442         pinnerService.onStart();
443 
444         // Verify that pin quota is zero
445         assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
446     }
447 
448     @Test
testPinAnonRegion()449     public void testPinAnonRegion() throws Exception {
450         setDeviceConfigPinnedAnonSize(32768);
451 
452         PinnerService pinnerService = new PinnerService(mContext, mInjector);
453         pinnerService.onStart();
454         waitForPinnerService(pinnerService);
455 
456         // Ensure the dump reflects the requested anon region.
457         int pinnedAnonSizeBytes = getPinnedAnonSize(pinnerService);
458         assertThat(pinnedAnonSizeBytes).isEqualTo(32768);
459 
460         unpinAll(pinnerService);
461     }
462 
463     @Test
testPinAnonRegionUpdatesOnConfigChange()464     public void testPinAnonRegionUpdatesOnConfigChange() throws Exception {
465         PinnerService pinnerService = new PinnerService(mContext, mInjector);
466         pinnerService.onStart();
467         waitForPinnerService(pinnerService);
468 
469         // Ensure the PinnerService updates itself when the associated DeviceConfig changes.
470         setDeviceConfigPinnedAnonSize(65536);
471         waitForPinnerService(pinnerService);
472         int pinnedAnonSizeBytes = getPinnedAnonSize(pinnerService);
473         assertThat(pinnedAnonSizeBytes).isEqualTo(65536);
474 
475         // Each update should be reflected in the reported status.
476         setDeviceConfigPinnedAnonSize(32768);
477         waitForPinnerService(pinnerService);
478         pinnedAnonSizeBytes = getPinnedAnonSize(pinnerService);
479         assertThat(pinnedAnonSizeBytes).isEqualTo(32768);
480 
481         setDeviceConfigPinnedAnonSize(0);
482         waitForPinnerService(pinnerService);
483         // An empty anon region should clear the associated status entry.
484         pinnedAnonSizeBytes = getPinnedAnonSize(pinnerService);
485         assertThat(pinnedAnonSizeBytes).isEqualTo(0);
486 
487         unpinAll(pinnerService);
488     }
489 }
490