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.Matchers.anyInt; 22 import static org.mockito.Mockito.doReturn; 23 import static org.mockito.Mockito.mock; 24 import static org.mockito.Mockito.spy; 25 import static org.mockito.Mockito.when; 26 27 import android.app.ActivityManagerInternal; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.content.res.Resources; 32 import android.os.Binder; 33 import android.os.Handler; 34 import android.os.Looper; 35 import android.testing.AndroidTestingRunner; 36 import android.testing.TestableContext; 37 import android.testing.TestableLooper; 38 import android.util.ArrayMap; 39 import android.util.ArraySet; 40 41 import androidx.test.InstrumentationRegistry; 42 43 import com.android.server.wm.ActivityTaskManagerInternal; 44 45 import org.junit.After; 46 import org.junit.Before; 47 import org.junit.Rule; 48 import org.junit.Test; 49 import org.junit.runner.RunWith; 50 import org.mockito.Mockito; 51 import org.mockito.MockitoAnnotations; 52 53 import java.io.BufferedReader; 54 import java.io.CharArrayWriter; 55 import java.io.FileDescriptor; 56 import java.io.PrintWriter; 57 import java.io.StringReader; 58 import java.lang.reflect.Constructor; 59 import java.lang.reflect.Field; 60 import java.lang.reflect.Method; 61 import java.util.Optional; 62 import java.util.concurrent.TimeUnit; 63 64 @RunWith(AndroidTestingRunner.class) 65 @TestableLooper.RunWithLooper 66 public class PinnerServiceTest { 67 private static final int KEY_CAMERA = 0; 68 private static final int KEY_HOME = 1; 69 private static final int KEY_ASSISTANT = 2; 70 71 private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2); 72 73 @Rule 74 public TestableContext mContext = 75 new TestableContext(InstrumentationRegistry.getContext(), null); 76 77 private final ArraySet<String> mUpdatedPackages = new ArraySet<>(); 78 private ResolveInfo mHomePackageResolveInfo; 79 80 @Before setUp()81 public void setUp() { 82 MockitoAnnotations.initMocks(this); 83 84 if (Looper.myLooper() == null) { 85 Looper.prepare(); 86 } 87 88 LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); 89 LocalServices.removeServiceForTest(ActivityManagerInternal.class); 90 91 ActivityTaskManagerInternal mockActivityTaskManagerInternal = mock( 92 ActivityTaskManagerInternal.class); 93 Intent homeIntent = getHomeIntent(); 94 95 doReturn(homeIntent).when(mockActivityTaskManagerInternal).getHomeIntent(); 96 LocalServices.addService(ActivityTaskManagerInternal.class, 97 mockActivityTaskManagerInternal); 98 99 ActivityManagerInternal mockActivityManagerInternal = mock(ActivityManagerInternal.class); 100 doReturn(true).when(mockActivityManagerInternal).isUidActive(anyInt()); 101 LocalServices.addService(ActivityManagerInternal.class, mockActivityManagerInternal); 102 103 mContext = spy(mContext); 104 105 // Get HOME (Launcher) package 106 mHomePackageResolveInfo = mContext.getPackageManager().resolveActivityAsUser(homeIntent, 107 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE 108 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); 109 mUpdatedPackages.add(mHomePackageResolveInfo.activityInfo.applicationInfo.packageName); 110 } 111 112 @After tearDown()113 public void tearDown() { 114 Mockito.framework().clearInlineMocks(); 115 } 116 getHomeIntent()117 private Intent getHomeIntent() { 118 Intent intent = new Intent(Intent.ACTION_MAIN); 119 intent.addCategory(Intent.CATEGORY_HOME); 120 intent.addCategory(Intent.CATEGORY_DEFAULT); 121 return intent; 122 } 123 unpinAll(PinnerService pinnerService)124 private void unpinAll(PinnerService pinnerService) throws Exception { 125 // unpin all packages 126 Method unpinAppMethod = PinnerService.class.getDeclaredMethod("unpinApp", int.class); 127 unpinAppMethod.setAccessible(true); 128 unpinAppMethod.invoke(pinnerService, KEY_HOME); 129 unpinAppMethod.invoke(pinnerService, KEY_CAMERA); 130 unpinAppMethod.invoke(pinnerService, KEY_ASSISTANT); 131 } 132 waitForPinnerService(PinnerService pinnerService)133 private void waitForPinnerService(PinnerService pinnerService) 134 throws NoSuchFieldException, IllegalAccessException { 135 // There's no notification/callback when pinning finished 136 // Block until pinner handler is done pinning and runs this empty runnable 137 Field pinnerHandlerField = PinnerService.class.getDeclaredField("mPinnerHandler"); 138 pinnerHandlerField.setAccessible(true); 139 Handler pinnerServiceHandler = (Handler) pinnerHandlerField.get(pinnerService); 140 pinnerServiceHandler.runWithScissors(() -> { 141 }, WAIT_FOR_PINNER_TIMEOUT); 142 } 143 getPinKeys(PinnerService pinnerService)144 private ArraySet<Integer> getPinKeys(PinnerService pinnerService) 145 throws NoSuchFieldException, IllegalAccessException { 146 Field pinKeysArrayField = PinnerService.class.getDeclaredField("mPinKeys"); 147 pinKeysArrayField.setAccessible(true); 148 return (ArraySet<Integer>) pinKeysArrayField.get(pinnerService); 149 } 150 getPinnedApps(PinnerService pinnerService)151 private ArrayMap<Integer, Object> getPinnedApps(PinnerService pinnerService) 152 throws NoSuchFieldException, IllegalAccessException { 153 Field pinnedAppsField = PinnerService.class.getDeclaredField("mPinnedApps"); 154 pinnedAppsField.setAccessible(true); 155 return (ArrayMap<Integer, Object>) pinnedAppsField.get( 156 pinnerService); 157 } 158 getPinnerServiceDump(PinnerService pinnerService)159 private String getPinnerServiceDump(PinnerService pinnerService) throws Exception { 160 Class<?> innerClass = Class.forName(PinnerService.class.getName() + "$BinderService"); 161 Constructor<?> ctor = innerClass.getDeclaredConstructor(PinnerService.class); 162 ctor.setAccessible(true); 163 Binder innerInstance = (Binder) ctor.newInstance(pinnerService); 164 CharArrayWriter cw = new CharArrayWriter(); 165 PrintWriter pw = new PrintWriter(cw, true); 166 Method dumpMethod = Binder.class.getDeclaredMethod("dump", FileDescriptor.class, 167 PrintWriter.class, String[].class); 168 dumpMethod.setAccessible(true); 169 dumpMethod.invoke(innerInstance, null, pw, null); 170 return cw.toString(); 171 } 172 getPinnedSize(PinnerService pinnerService)173 private int getPinnedSize(PinnerService pinnerService) throws Exception { 174 final String totalSizeToken = "Total size: "; 175 String dumpOutput = getPinnerServiceDump(pinnerService); 176 BufferedReader bufReader = new BufferedReader(new StringReader(dumpOutput)); 177 Optional<Integer> size = bufReader.lines().filter(s -> s.contains(totalSizeToken)) 178 .map(s -> Integer.valueOf(s.substring(totalSizeToken.length()))).findAny(); 179 return size.orElse(-1); 180 } 181 182 @Test testPinHomeApp()183 public void testPinHomeApp() throws Exception { 184 // Enable HOME app pinning 185 Resources res = mock(Resources.class); 186 doReturn(true).when(res).getBoolean(com.android.internal.R.bool.config_pinnerHomeApp); 187 when(mContext.getResources()).thenReturn(res); 188 PinnerService pinnerService = new PinnerService(mContext); 189 190 ArraySet<Integer> pinKeys = getPinKeys(pinnerService); 191 assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME); 192 193 pinnerService.update(mUpdatedPackages, true); 194 195 waitForPinnerService(pinnerService); 196 197 ArrayMap<Integer, Object> pinnedApps = getPinnedApps(pinnerService); 198 assertThat(pinnedApps.get(KEY_HOME)).isNotNull(); 199 200 // Check if dump() reports total pinned bytes 201 int totalPinnedSizeBytes = getPinnedSize(pinnerService); 202 assertThat(totalPinnedSizeBytes).isGreaterThan(0); 203 204 // Make sure pinned files are unmapped 205 unpinAll(pinnerService); 206 } 207 208 @Test testPinHomeAppOnBootCompleted()209 public void testPinHomeAppOnBootCompleted() throws Exception { 210 // Enable HOME app pinning 211 Resources res = mock(Resources.class); 212 doReturn(true).when(res).getBoolean(com.android.internal.R.bool.config_pinnerHomeApp); 213 when(mContext.getResources()).thenReturn(res); 214 PinnerService pinnerService = new PinnerService(mContext); 215 216 ArraySet<Integer> pinKeys = getPinKeys(pinnerService); 217 assertThat(pinKeys.valueAt(0)).isEqualTo(KEY_HOME); 218 219 pinnerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); 220 221 waitForPinnerService(pinnerService); 222 223 ArrayMap<Integer, Object> pinnedApps = getPinnedApps(pinnerService); 224 assertThat(pinnedApps.get(KEY_HOME)).isNotNull(); 225 226 // Check if dump() reports total pinned bytes 227 int totalPinnedSizeBytes = getPinnedSize(pinnerService); 228 assertThat(totalPinnedSizeBytes).isGreaterThan(0); 229 230 // Make sure pinned files are unmapped 231 unpinAll(pinnerService); 232 } 233 234 @Test testNothingToPin()235 public void testNothingToPin() throws Exception { 236 // No package enabled for pinning 237 Resources res = mock(Resources.class); 238 when(mContext.getResources()).thenReturn(res); 239 PinnerService pinnerService = new PinnerService(mContext); 240 241 ArraySet<Integer> pinKeys = getPinKeys(pinnerService); 242 assertThat(pinKeys).isEmpty(); 243 244 pinnerService.update(mUpdatedPackages, true); 245 246 waitForPinnerService(pinnerService); 247 248 ArrayMap<Integer, Object> pinnedApps = getPinnedApps(pinnerService); 249 assertThat(pinnedApps).isEmpty(); 250 251 // Check if dump() reports total pinned bytes 252 int totalPinnedSizeBytes = getPinnedSize(pinnerService); 253 assertThat(totalPinnedSizeBytes).isEqualTo(0); 254 255 // Make sure pinned files are unmapped 256 unpinAll(pinnerService); 257 } 258 259 // TODO: Add test to check that the pages we expect to be pinned are actually pinned 260 261 } 262