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