| /* |
| * 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<String> 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<Integer> getPinKeys(PinnerService pinnerService) |
| throws NoSuchFieldException, IllegalAccessException { |
| Field pinKeysArrayField = PinnerService.class.getDeclaredField("mPinKeys"); |
| pinKeysArrayField.setAccessible(true); |
| return (ArraySet<Integer>) pinKeysArrayField.get(pinnerService); |
| } |
| |
| private ArrayMap<Integer, Object> getPinnedApps(PinnerService pinnerService) |
| throws NoSuchFieldException, IllegalAccessException { |
| Field pinnedAppsField = PinnerService.class.getDeclaredField("mPinnedApps"); |
| pinnedAppsField.setAccessible(true); |
| return (ArrayMap<Integer, Object>) 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<Integer> 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<Integer> pinKeys = getPinKeys(pinnerService); |
| assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME); |
| |
| pinnerService.update(mUpdatedPackages, true); |
| |
| waitForPinnerService(pinnerService); |
| |
| ArrayMap<Integer, Object> 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<Integer> pinKeys = getPinKeys(pinnerService); |
| assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME); |
| |
| pinnerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); |
| |
| waitForPinnerService(pinnerService); |
| |
| ArrayMap<Integer, Object> 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<Integer> pinKeys = getPinKeys(pinnerService); |
| assertThat(pinKeys).isEmpty(); |
| |
| pinnerService.update(mUpdatedPackages, true); |
| |
| waitForPinnerService(pinnerService); |
| |
| ArrayMap<Integer, Object> 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 |
| |
| } |