/* * Copyright (C) 2020 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.server; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.app.ActivityManagerInternal; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.os.Binder; import android.os.Handler; import android.os.Looper; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; import android.util.ArrayMap; import android.util.ArraySet; import androidx.test.InstrumentationRegistry; import com.android.server.wm.ActivityTaskManagerInternal; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.io.BufferedReader; import java.io.CharArrayWriter; import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.StringReader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Optional; import java.util.concurrent.TimeUnit; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class PinnerServiceTest { private static final int KEY_CAMERA = 0; private static final int KEY_HOME = 1; private static final int KEY_ASSISTANT = 2; private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2); @Rule public TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext(), null); private final ArraySet mUpdatedPackages = new ArraySet<>(); private ResolveInfo mHomePackageResolveInfo; @Before public void setUp() { MockitoAnnotations.initMocks(this); if (Looper.myLooper() == null) { Looper.prepare(); } LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); LocalServices.removeServiceForTest(ActivityManagerInternal.class); ActivityTaskManagerInternal mockActivityTaskManagerInternal = mock( ActivityTaskManagerInternal.class); Intent homeIntent = getHomeIntent(); doReturn(homeIntent).when(mockActivityTaskManagerInternal).getHomeIntent(); LocalServices.addService(ActivityTaskManagerInternal.class, mockActivityTaskManagerInternal); ActivityManagerInternal mockActivityManagerInternal = mock(ActivityManagerInternal.class); doReturn(true).when(mockActivityManagerInternal).isUidActive(anyInt()); LocalServices.addService(ActivityManagerInternal.class, mockActivityManagerInternal); mContext = spy(mContext); // Get HOME (Launcher) package mHomePackageResolveInfo = mContext.getPackageManager().resolveActivityAsUser(homeIntent, PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); mUpdatedPackages.add(mHomePackageResolveInfo.activityInfo.applicationInfo.packageName); } @After public void tearDown() { Mockito.framework().clearInlineMocks(); } private Intent getHomeIntent() { Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); intent.addCategory(Intent.CATEGORY_DEFAULT); return intent; } private void unpinAll(PinnerService pinnerService) throws Exception { // unpin all packages Method unpinAppMethod = PinnerService.class.getDeclaredMethod("unpinApp", int.class); unpinAppMethod.setAccessible(true); unpinAppMethod.invoke(pinnerService, KEY_HOME); unpinAppMethod.invoke(pinnerService, KEY_CAMERA); unpinAppMethod.invoke(pinnerService, KEY_ASSISTANT); } private void waitForPinnerService(PinnerService pinnerService) throws NoSuchFieldException, IllegalAccessException { // There's no notification/callback when pinning finished // Block until pinner handler is done pinning and runs this empty runnable Field pinnerHandlerField = PinnerService.class.getDeclaredField("mPinnerHandler"); pinnerHandlerField.setAccessible(true); Handler pinnerServiceHandler = (Handler) pinnerHandlerField.get(pinnerService); pinnerServiceHandler.runWithScissors(() -> { }, WAIT_FOR_PINNER_TIMEOUT); } private ArraySet getPinKeys(PinnerService pinnerService) throws NoSuchFieldException, IllegalAccessException { Field pinKeysArrayField = PinnerService.class.getDeclaredField("mPinKeys"); pinKeysArrayField.setAccessible(true); return (ArraySet) pinKeysArrayField.get(pinnerService); } private ArrayMap getPinnedApps(PinnerService pinnerService) throws NoSuchFieldException, IllegalAccessException { Field pinnedAppsField = PinnerService.class.getDeclaredField("mPinnedApps"); pinnedAppsField.setAccessible(true); return (ArrayMap) pinnedAppsField.get( pinnerService); } private String getPinnerServiceDump(PinnerService pinnerService) throws Exception { Class innerClass = Class.forName(PinnerService.class.getName() + "$BinderService"); Constructor ctor = innerClass.getDeclaredConstructor(PinnerService.class); ctor.setAccessible(true); Binder innerInstance = (Binder) ctor.newInstance(pinnerService); CharArrayWriter cw = new CharArrayWriter(); PrintWriter pw = new PrintWriter(cw, true); Method dumpMethod = Binder.class.getDeclaredMethod("dump", FileDescriptor.class, PrintWriter.class, String[].class); dumpMethod.setAccessible(true); dumpMethod.invoke(innerInstance, null, pw, null); return cw.toString(); } private int getPinnedSize(PinnerService pinnerService) throws Exception { final String totalSizeToken = "Total size: "; String dumpOutput = getPinnerServiceDump(pinnerService); BufferedReader bufReader = new BufferedReader(new StringReader(dumpOutput)); Optional size = bufReader.lines().filter(s -> s.contains(totalSizeToken)) .map(s -> Integer.valueOf(s.substring(totalSizeToken.length()))).findAny(); return size.orElse(-1); } @Test public void testPinHomeApp() throws Exception { // Enable HOME app pinning Resources res = mock(Resources.class); doReturn(true).when(res).getBoolean(com.android.internal.R.bool.config_pinnerHomeApp); when(mContext.getResources()).thenReturn(res); PinnerService pinnerService = new PinnerService(mContext); ArraySet pinKeys = getPinKeys(pinnerService); assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME); pinnerService.update(mUpdatedPackages, true); waitForPinnerService(pinnerService); ArrayMap pinnedApps = getPinnedApps(pinnerService); assertThat(pinnedApps.get(KEY_HOME)).isNotNull(); // Check if dump() reports total pinned bytes int totalPinnedSizeBytes = getPinnedSize(pinnerService); assertThat(totalPinnedSizeBytes).isGreaterThan(0); // Make sure pinned files are unmapped unpinAll(pinnerService); } @Test public void testPinHomeAppOnBootCompleted() throws Exception { // Enable HOME app pinning Resources res = mock(Resources.class); doReturn(true).when(res).getBoolean(com.android.internal.R.bool.config_pinnerHomeApp); when(mContext.getResources()).thenReturn(res); PinnerService pinnerService = new PinnerService(mContext); ArraySet pinKeys = getPinKeys(pinnerService); assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME); pinnerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); waitForPinnerService(pinnerService); ArrayMap pinnedApps = getPinnedApps(pinnerService); assertThat(pinnedApps.get(KEY_HOME)).isNotNull(); // Check if dump() reports total pinned bytes int totalPinnedSizeBytes = getPinnedSize(pinnerService); assertThat(totalPinnedSizeBytes).isGreaterThan(0); // Make sure pinned files are unmapped unpinAll(pinnerService); } @Test public void testNothingToPin() throws Exception { // No package enabled for pinning Resources res = mock(Resources.class); when(mContext.getResources()).thenReturn(res); PinnerService pinnerService = new PinnerService(mContext); ArraySet pinKeys = getPinKeys(pinnerService); assertThat(pinKeys).isEmpty(); pinnerService.update(mUpdatedPackages, true); waitForPinnerService(pinnerService); ArrayMap pinnedApps = getPinnedApps(pinnerService); assertThat(pinnedApps).isEmpty(); // Check if dump() reports total pinned bytes int totalPinnedSizeBytes = getPinnedSize(pinnerService); assertThat(totalPinnedSizeBytes).isEqualTo(0); // Make sure pinned files are unmapped unpinAll(pinnerService); } // TODO: Add test to check that the pages we expect to be pinned are actually pinned }