Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1d7d93f8 authored by Treehugger Robot's avatar Treehugger Robot Committed by Automerger Merge Worker
Browse files

Merge changes I8fffd952,Ief6690bb am: b4203793 am: d16fff5b am: 476343a4 am: 947ce6df

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1550435

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: Ia19fca9b289df5ae8b0fd941ab78a56b99cccdaf
parents cb9100c6 947ce6df
Loading
Loading
Loading
Loading
+242 −4
Original line number Diff line number Diff line
@@ -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
    }
}
+109 −0
Original line number Diff line number Diff line
/*
 * 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("");
    }
}
+168 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}