diff options
3 files changed, 519 insertions, 4 deletions
diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java index e8e83cc967b9..508bb01e50a8 100644 --- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java +++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java @@ -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 + } } diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationShellCommand.java b/services/core/java/com/android/server/apphibernation/AppHibernationShellCommand.java new file mode 100644 index 000000000000..869885e28958 --- /dev/null +++ b/services/core/java/com/android/server/apphibernation/AppHibernationShellCommand.java @@ -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(""); + } +} diff --git a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java new file mode 100644 index 000000000000..d0370b6c25e9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java @@ -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; + } +} |