Merge changes I8fffd952,Ief6690bb

* changes:
  Add shell commands for app hibernation service
  Add basic hibernation state management
This commit is contained in:
Treehugger Robot
2021-01-27 01:36:43 +00:00
committed by Gerrit Code Review
3 changed files with 519 additions and 4 deletions

View File

@@ -16,23 +16,64 @@
package com.android.server.apphibernation;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
import static android.content.Intent.ACTION_USER_ADDED;
import static android.content.Intent.ACTION_USER_REMOVED;
import static android.content.Intent.EXTRA_REPLACING;
import static android.content.pm.PackageManager.MATCH_ALL;
import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.IActivityManager;
import android.apphibernation.IAppHibernationService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.util.ArrayMap;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
import java.io.FileDescriptor;
import java.util.List;
import java.util.Map;
/**
* System service that manages app hibernation state, a state apps can enter that means they are
* not being actively used and can be optimized for storage. The actual policy for determining
* if an app should hibernate is managed by PermissionController code.
*/
public final class AppHibernationService extends SystemService {
private static final String TAG = "AppHibernationService";
/**
* Lock for accessing any in-memory hibernation state
*/
private final Object mLock = new Object();
private final Context mContext;
private final IPackageManager mIPackageManager;
private final IActivityManager mIActivityManager;
private final UserManager mUserManager;
@GuardedBy("mLock")
private final SparseArray<Map<String, UserPackageState>> mUserStates = new SparseArray<>();
/**
* Initializes the system service.
@@ -44,8 +85,32 @@ public final class AppHibernationService extends SystemService {
* @param context The system server context.
*/
public AppHibernationService(@NonNull Context context) {
this(context, IPackageManager.Stub.asInterface(ServiceManager.getService("package")),
ActivityManager.getService(),
context.getSystemService(UserManager.class));
}
@VisibleForTesting
AppHibernationService(@NonNull Context context, IPackageManager packageManager,
IActivityManager activityManager, UserManager userManager) {
super(context);
mContext = context;
mIPackageManager = packageManager;
mIActivityManager = activityManager;
mUserManager = userManager;
final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_USER_ADDED);
intentFilter.addAction(ACTION_USER_REMOVED);
userAllContext.registerReceiver(mBroadcastReceiver, intentFilter);
intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_PACKAGE_ADDED);
intentFilter.addAction(ACTION_PACKAGE_REMOVED);
intentFilter.addDataScheme("package");
userAllContext.registerReceiver(mBroadcastReceiver, intentFilter);
}
@Override
@@ -53,6 +118,19 @@ public final class AppHibernationService extends SystemService {
publishBinderService(Context.APP_HIBERNATION_SERVICE, mServiceStub);
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_BOOT_COMPLETED) {
synchronized (mLock) {
final List<UserInfo> users = mUserManager.getUsers();
// TODO: Pull from persistent disk storage. For now, just make from scratch.
for (UserInfo user : users) {
addUserPackageStatesL(user.id);
}
}
}
}
/**
* Whether a package is hibernating for a given user.
*
@@ -61,8 +139,20 @@ public final class AppHibernationService extends SystemService {
* @return true if package is hibernating for the user
*/
public boolean isHibernating(String packageName, int userId) {
// Stub
throw new UnsupportedOperationException("Hibernation state management not implemented yet");
userId = handleIncomingUser(userId, "isHibernating");
synchronized (mLock) {
final Map<String, UserPackageState> packageStates = mUserStates.get(userId);
if (packageStates == null) {
throw new IllegalArgumentException("No user associated with user id " + userId);
}
final UserPackageState pkgState = packageStates.get(packageName);
if (pkgState == null) {
throw new IllegalArgumentException(
String.format("Package %s is not installed for user %s",
packageName, userId));
}
return pkgState != null ? pkgState.hibernated : null;
}
}
/**
@@ -73,8 +163,108 @@ public final class AppHibernationService extends SystemService {
* @param isHibernating new hibernation state
*/
public void setHibernating(String packageName, int userId, boolean isHibernating) {
// Stub
throw new UnsupportedOperationException("Hibernation state management not implemented yet");
userId = handleIncomingUser(userId, "setHibernating");
synchronized (mLock) {
if (!mUserStates.contains(userId)) {
throw new IllegalArgumentException("No user associated with user id " + userId);
}
Map<String, UserPackageState> packageStates = mUserStates.get(userId);
UserPackageState pkgState = packageStates.get(packageName);
if (pkgState == null) {
throw new IllegalArgumentException(
String.format("Package %s is not installed for user %s",
packageName, userId));
}
if (pkgState.hibernated == isHibernating) {
return;
}
final long caller = Binder.clearCallingIdentity();
try {
if (isHibernating) {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "hibernatePackage");
mIActivityManager.forceStopPackage(packageName, userId);
mIPackageManager.deleteApplicationCacheFilesAsUser(packageName, userId,
null /* observer */);
} else {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "unhibernatePackage");
mIPackageManager.setPackageStoppedState(packageName, false, userId);
}
pkgState.hibernated = isHibernating;
} catch (RemoteException e) {
throw new IllegalStateException(
"Failed to hibernate due to manager not being available", e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
Binder.restoreCallingIdentity(caller);
}
// TODO: Support package level hibernation when package is hibernating for all users
}
}
/**
* Populates {@link #mUserStates} with the users installed packages. The caller should hold
* {@link #mLock}.
*
* @param userId user id to add installed packages for
*/
private void addUserPackageStatesL(int userId) {
Map<String, UserPackageState> packages = new ArrayMap<>();
List<PackageInfo> packageList;
try {
packageList = mIPackageManager.getInstalledPackages(MATCH_ALL, userId).getList();
} catch (RemoteException e) {
throw new IllegalStateException("Package manager not available.", e);
}
for (PackageInfo pkg : packageList) {
packages.put(pkg.packageName, new UserPackageState());
}
mUserStates.put(userId, packages);
}
private void onUserAdded(int userId) {
synchronized (mLock) {
addUserPackageStatesL(userId);
}
}
private void onUserRemoved(int userId) {
synchronized (mLock) {
mUserStates.remove(userId);
}
}
private void onPackageAdded(@NonNull String packageName, int userId) {
synchronized (mLock) {
mUserStates.get(userId).put(packageName, new UserPackageState());
}
}
private void onPackageRemoved(@NonNull String packageName, int userId) {
synchronized (mLock) {
mUserStates.get(userId).remove(packageName);
}
}
/**
* Private helper method to get the real user id and enforce permission checks.
*
* @param userId user id to handle
* @param name name to use for exceptions
* @return real user id
*/
private int handleIncomingUser(int userId, @NonNull String name) {
int callingUid = Binder.getCallingUid();
try {
return mIActivityManager.handleIncomingUser(Binder.getCallingPid(), callingUid, userId,
false /* allowAll */, true /* requireFull */, name, null);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
private final AppHibernationServiceStub mServiceStub = new AppHibernationServiceStub(this);
@@ -95,8 +285,48 @@ public final class AppHibernationService extends SystemService {
public void setHibernating(String packageName, int userId, boolean isHibernating) {
mService.setHibernating(packageName, userId, isHibernating);
}
@Override
public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
@Nullable FileDescriptor err, @NonNull String[] args,
@Nullable ShellCallback callback, @NonNull ResultReceiver resultReceiver) {
new AppHibernationShellCommand(mService).exec(this, in, out, err, args, callback,
resultReceiver);
}
}
// Broadcast receiver for user and package add/removal events
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
if (userId == UserHandle.USER_NULL) {
return;
}
final String action = intent.getAction();
if (ACTION_USER_ADDED.equals(action)) {
onUserAdded(userId);
}
if (ACTION_USER_REMOVED.equals(action)) {
onUserRemoved(userId);
}
if (ACTION_PACKAGE_ADDED.equals(action) || ACTION_PACKAGE_REMOVED.equals(action)) {
final String packageName = intent.getData().getSchemeSpecificPart();
if (intent.getBooleanExtra(EXTRA_REPLACING, false)) {
// Package removal/add is part of an update, so no need to modify package state.
return;
}
if (ACTION_PACKAGE_ADDED.equals(action)) {
onPackageAdded(packageName, userId);
} else if (ACTION_PACKAGE_REMOVED.equals(action)) {
onPackageRemoved(packageName, userId);
}
}
}
};
/**
* Whether app hibernation is enabled on this device.
*
@@ -108,4 +338,12 @@ public final class AppHibernationService extends SystemService {
AppHibernationConstants.KEY_APP_HIBERNATION_ENABLED,
false /* defaultValue */);
}
/**
* Data class that contains hibernation state info of a package for a user.
*/
private static final class UserPackageState {
public boolean hibernated;
// TODO: Track whether hibernation is exempted by the user
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2021 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.apphibernation;
import android.os.ShellCommand;
import android.os.UserHandle;
import android.text.TextUtils;
import java.io.PrintWriter;
/**
* Shell command implementation for {@link AppHibernationService}.
*/
final class AppHibernationShellCommand extends ShellCommand {
private static final String USER_OPT = "--user";
private static final int SUCCESS = 0;
private static final int ERROR = -1;
private final AppHibernationService mService;
AppHibernationShellCommand(AppHibernationService service) {
mService = service;
}
@Override
public int onCommand(String cmd) {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
switch (cmd) {
case "set-state":
return runSetState();
case "get-state":
return runGetState();
default:
return handleDefaultCommands(cmd);
}
}
private int runSetState() {
int userId = parseUserOption();
String pkg = getNextArgRequired();
if (pkg == null) {
getErrPrintWriter().println("Error: no package specified");
return ERROR;
}
String newStateRaw = getNextArgRequired();
if (newStateRaw == null) {
getErrPrintWriter().println("Error: No state to set specified");
return ERROR;
}
boolean newState = Boolean.parseBoolean(newStateRaw);
mService.setHibernating(pkg, userId, newState);
return SUCCESS;
}
private int runGetState() {
int userId = parseUserOption();
String pkg = getNextArgRequired();
if (pkg == null) {
getErrPrintWriter().println("Error: No package specified");
return ERROR;
}
boolean isHibernating = mService.isHibernating(pkg, userId);
final PrintWriter pw = getOutPrintWriter();
pw.println(isHibernating);
return SUCCESS;
}
private int parseUserOption() {
String option = getNextOption();
if (TextUtils.equals(option, USER_OPT)) {
return UserHandle.parseUserArg(getNextArgRequired());
}
return UserHandle.USER_CURRENT;
}
@Override
public void onHelp() {
final PrintWriter pw = getOutPrintWriter();
pw.println("App hibernation (app_hibernation) commands: ");
pw.println(" help");
pw.println(" Print this help text.");
pw.println("");
pw.println(" set-state [--user USER_ID] PACKAGE true|false");
pw.println(" Sets the hibernation state of the package to value specified");
pw.println("");
pw.println(" get-state [--user USER_ID] PACKAGE");
pw.println(" Gets the hibernation state of the package");
pw.println("");
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2021 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.apphibernation;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.returnsArgAt;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import android.app.IActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.ParceledListSlice;
import android.content.pm.UserInfo;
import android.net.Uri;
import android.os.RemoteException;
import android.os.UserManager;
import androidx.test.filters.SmallTest;
import com.android.server.SystemService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
/**
* Tests for {@link com.android.server.apphibernation.AppHibernationService}
*/
@SmallTest
public final class AppHibernationServiceTest {
private static final String PACKAGE_SCHEME = "package";
private static final String PACKAGE_NAME_1 = "package1";
private static final String PACKAGE_NAME_2 = "package2";
private static final int USER_ID_1 = 1;
private static final int USER_ID_2 = 2;
private AppHibernationService mAppHibernationService;
private BroadcastReceiver mBroadcastReceiver;
@Mock
private Context mContext;
@Mock
private IPackageManager mIPackageManager;
@Mock
private IActivityManager mIActivityManager;
@Mock
private UserManager mUserManager;
@Captor
private ArgumentCaptor<BroadcastReceiver> mReceiverCaptor;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
doReturn(mContext).when(mContext).createContextAsUser(any(), anyInt());
mAppHibernationService = new AppHibernationService(mContext, mIPackageManager,
mIActivityManager, mUserManager);
verify(mContext, times(2)).registerReceiver(mReceiverCaptor.capture(), any());
mBroadcastReceiver = mReceiverCaptor.getValue();
List<UserInfo> userList = new ArrayList<>();
userList.add(new UserInfo(USER_ID_1, "user 1", 0 /* flags */));
doReturn(userList).when(mUserManager).getUsers();
List<PackageInfo> userPackages = new ArrayList<>();
userPackages.add(makePackageInfo(PACKAGE_NAME_1));
doReturn(new ParceledListSlice<>(userPackages)).when(mIPackageManager)
.getInstalledPackages(anyInt(), eq(USER_ID_1));
doAnswer(returnsArgAt(2)).when(mIActivityManager).handleIncomingUser(anyInt(), anyInt(),
anyInt(), anyBoolean(), anyBoolean(), any(), any());
mAppHibernationService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
}
@Test
public void testSetHibernating_packageIsHibernating() {
// WHEN we hibernate a package for a user
mAppHibernationService.setHibernating(PACKAGE_NAME_1, USER_ID_1, true);
// THEN the package is marked hibernating for the user
assertTrue(mAppHibernationService.isHibernating(PACKAGE_NAME_1, USER_ID_1));
}
@Test
public void testSetHibernating_newPackageAdded_packageIsHibernating() {
// WHEN a new package is added and it is hibernated
Intent intent = new Intent(Intent.ACTION_PACKAGE_ADDED,
Uri.fromParts(PACKAGE_SCHEME, PACKAGE_NAME_2, null /* fragment */));
intent.putExtra(Intent.EXTRA_USER_HANDLE, USER_ID_1);
mBroadcastReceiver.onReceive(mContext, intent);
mAppHibernationService.setHibernating(PACKAGE_NAME_2, USER_ID_1, true);
// THEN the new package is hibernated
assertTrue(mAppHibernationService.isHibernating(PACKAGE_NAME_2, USER_ID_1));
}
@Test
public void testSetHibernating_newUserAdded_packageIsHibernating() throws RemoteException {
// WHEN a new user is added and a package from the user is hibernated
List<PackageInfo> userPackages = new ArrayList<>();
userPackages.add(makePackageInfo(PACKAGE_NAME_1));
doReturn(new ParceledListSlice<>(userPackages)).when(mIPackageManager)
.getInstalledPackages(anyInt(), eq(USER_ID_2));
Intent intent = new Intent(Intent.ACTION_USER_ADDED);
intent.putExtra(Intent.EXTRA_USER_HANDLE, USER_ID_2);
mBroadcastReceiver.onReceive(mContext, intent);
mAppHibernationService.setHibernating(PACKAGE_NAME_1, USER_ID_2, true);
// THEN the new user's package is hibernated
assertTrue(mAppHibernationService.isHibernating(PACKAGE_NAME_1, USER_ID_2));
}
@Test
public void testIsHibernating_packageReplaced_stillReturnsHibernating() {
// GIVEN a package is currently hibernated
mAppHibernationService.setHibernating(PACKAGE_NAME_1, USER_ID_1, true);
// WHEN the package is removed but marked as replacing
Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED,
Uri.fromParts(PACKAGE_SCHEME, PACKAGE_NAME_2, null /* fragment */));
intent.putExtra(Intent.EXTRA_USER_HANDLE, USER_ID_1);
intent.putExtra(Intent.EXTRA_REPLACING, true);
mBroadcastReceiver.onReceive(mContext, intent);
// THEN the package is still hibernating
assertTrue(mAppHibernationService.isHibernating(PACKAGE_NAME_1, USER_ID_1));
}
private static PackageInfo makePackageInfo(String packageName) {
PackageInfo pkg = new PackageInfo();
pkg.packageName = packageName;
return pkg;
}
}