blob: ed0a550015cba94f95b18a7eee5646628295d825 [file] [log] [blame]
/*
* 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
}