| /* |
| * 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 android.app.AppOpsManager.OP_NONE; |
| import static android.content.Intent.ACTION_PACKAGE_ADDED; |
| import static android.content.Intent.ACTION_PACKAGE_REMOVED; |
| import static android.content.Intent.EXTRA_REMOVED_FOR_ALL_USERS; |
| import static android.content.Intent.EXTRA_REPLACING; |
| import static android.content.pm.PackageManager.MATCH_ANY_USER; |
| import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION; |
| |
| import static com.android.server.apphibernation.AppHibernationConstants.KEY_APP_HIBERNATION_ENABLED; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.ActivityThread; |
| import android.app.IActivityManager; |
| import android.app.StatsManager; |
| import android.app.StatsManager.StatsPullAtomCallback; |
| import android.app.usage.StorageStats; |
| import android.app.usage.StorageStatsManager; |
| import android.app.usage.UsageEvents; |
| import android.app.usage.UsageStatsManagerInternal; |
| import android.app.usage.UsageStatsManagerInternal.UsageEventListener; |
| import android.apphibernation.HibernationStats; |
| import android.apphibernation.IAppHibernationService; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManagerInternal; |
| import android.content.pm.UserInfo; |
| import android.os.Binder; |
| import android.os.Environment; |
| 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.provider.DeviceConfig.Properties; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.util.StatsEvent; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.DumpUtils; |
| import com.android.internal.util.FrameworkStatsLog; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.server.LocalServices; |
| import com.android.server.SystemService; |
| |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.ScheduledExecutorService; |
| |
| /** |
| * 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"; |
| private static final long PACKAGE_MATCH_FLAGS = |
| PackageManager.MATCH_DIRECT_BOOT_AWARE |
| | PackageManager.MATCH_DIRECT_BOOT_UNAWARE |
| | PackageManager.MATCH_UNINSTALLED_PACKAGES |
| | PackageManager.MATCH_DISABLED_COMPONENTS |
| | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS |
| | PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS; |
| |
| /** |
| * Lock for accessing any in-memory hibernation state |
| */ |
| private final Object mLock = new Object(); |
| private final Context mContext; |
| private final IPackageManager mIPackageManager; |
| private final PackageManagerInternal mPackageManagerInternal; |
| private final IActivityManager mIActivityManager; |
| private final UserManager mUserManager; |
| private final StorageStatsManager mStorageStatsManager; |
| |
| @GuardedBy("mLock") |
| private final SparseArray<Map<String, UserLevelState>> mUserStates = new SparseArray<>(); |
| private final SparseArray<HibernationStateDiskStore<UserLevelState>> mUserDiskStores = |
| new SparseArray<>(); |
| @GuardedBy("mLock") |
| private final Map<String, GlobalLevelState> mGlobalHibernationStates = new ArrayMap<>(); |
| private final HibernationStateDiskStore<GlobalLevelState> mGlobalLevelHibernationDiskStore; |
| private final Injector mInjector; |
| private final Executor mBackgroundExecutor; |
| private final boolean mOatArtifactDeletionEnabled; |
| |
| @VisibleForTesting |
| public static boolean sIsServiceEnabled; |
| |
| /** |
| * Initializes the system service. |
| * <p> |
| * Subclasses must define a single argument constructor that accepts the context |
| * and passes it to super. |
| * </p> |
| * |
| * @param context The system server context. |
| */ |
| public AppHibernationService(@NonNull Context context) { |
| this(new InjectorImpl(context)); |
| } |
| |
| @VisibleForTesting |
| AppHibernationService(@NonNull Injector injector) { |
| super(injector.getContext()); |
| mContext = injector.getContext(); |
| mIPackageManager = injector.getPackageManager(); |
| mPackageManagerInternal = injector.getPackageManagerInternal(); |
| mIActivityManager = injector.getActivityManager(); |
| mUserManager = injector.getUserManager(); |
| mStorageStatsManager = injector.getStorageStatsManager(); |
| mGlobalLevelHibernationDiskStore = injector.getGlobalLevelDiskStore(); |
| mBackgroundExecutor = injector.getBackgroundExecutor(); |
| mOatArtifactDeletionEnabled = injector.isOatArtifactDeletionEnabled(); |
| mInjector = injector; |
| |
| final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); |
| |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(ACTION_PACKAGE_ADDED); |
| intentFilter.addAction(ACTION_PACKAGE_REMOVED); |
| intentFilter.addDataScheme("package"); |
| userAllContext.registerReceiver(mBroadcastReceiver, intentFilter); |
| LocalServices.addService(AppHibernationManagerInternal.class, mLocalService); |
| mInjector.getUsageStatsManagerInternal().registerListener(mUsageEventListener); |
| } |
| |
| @Override |
| public void onStart() { |
| publishBinderService(Context.APP_HIBERNATION_SERVICE, mServiceStub); |
| } |
| |
| @Override |
| public void onBootPhase(int phase) { |
| if (phase == PHASE_BOOT_COMPLETED) { |
| mBackgroundExecutor.execute(() -> { |
| List<GlobalLevelState> states = |
| mGlobalLevelHibernationDiskStore.readHibernationStates(); |
| synchronized (mLock) { |
| initializeGlobalHibernationStates(states); |
| } |
| }); |
| } |
| if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { |
| sIsServiceEnabled = isDeviceConfigAppHibernationEnabled(); |
| DeviceConfig.addOnPropertiesChangedListener( |
| NAMESPACE_APP_HIBERNATION, |
| ActivityThread.currentApplication().getMainExecutor(), |
| this::onDeviceConfigChanged); |
| final StatsManager statsManager = getContext().getSystemService(StatsManager.class); |
| final StatsPullAtomCallbackImpl pullAtomCallback = new StatsPullAtomCallbackImpl(); |
| statsManager.setPullAtomCallback( |
| FrameworkStatsLog.USER_LEVEL_HIBERNATED_APPS, |
| /* metadata */ null, // use default PullAtomMetadata values |
| mBackgroundExecutor, |
| pullAtomCallback); |
| statsManager.setPullAtomCallback( |
| FrameworkStatsLog.GLOBAL_HIBERNATED_APPS, |
| /* metadata */ null, // use default PullAtomMetadata values |
| mBackgroundExecutor, |
| pullAtomCallback); |
| } |
| } |
| |
| /** |
| * Whether global hibernation should delete ART ahead-of-time compilation artifacts and prevent |
| * package manager from re-optimizing the APK. |
| */ |
| private boolean isOatArtifactDeletionEnabled() { |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.MANAGE_APP_HIBERNATION, |
| "Caller does not have MANAGE_APP_HIBERNATION permission."); |
| return mOatArtifactDeletionEnabled; |
| } |
| |
| /** |
| * Whether a package is hibernating for a given user. |
| * |
| * @param packageName the package to check |
| * @param userId the user to check |
| * @return true if package is hibernating for the user |
| */ |
| boolean isHibernatingForUser(String packageName, int userId) { |
| String methodName = "isHibernatingForUser"; |
| if (!sIsServiceEnabled) { |
| return false; |
| } |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.MANAGE_APP_HIBERNATION, |
| "Caller did not have permission while calling " + methodName); |
| userId = handleIncomingUser(userId, methodName); |
| synchronized (mLock) { |
| // Don't log as this method can be called before user states exist as part of the |
| // force-stop check. |
| if (!checkUserStatesExist(userId, methodName, /* shouldLog= */ false)) { |
| return false; |
| } |
| final Map<String, UserLevelState> packageStates = mUserStates.get(userId); |
| final UserLevelState pkgState = packageStates.get(packageName); |
| if (pkgState == null |
| || !mPackageManagerInternal.canQueryPackage( |
| Binder.getCallingUid(), packageName)) { |
| return false; |
| } |
| return pkgState.hibernated; |
| } |
| } |
| |
| /** |
| * Whether a package is hibernated globally. This only occurs when a package is hibernating for |
| * all users and allows us to make optimizations at the package or APK level. |
| * |
| * @param packageName package to check |
| */ |
| boolean isHibernatingGlobally(String packageName) { |
| if (!sIsServiceEnabled) { |
| return false; |
| } |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.MANAGE_APP_HIBERNATION, |
| "Caller does not have MANAGE_APP_HIBERNATION permission."); |
| synchronized (mLock) { |
| GlobalLevelState state = mGlobalHibernationStates.get(packageName); |
| if (state == null |
| || !mPackageManagerInternal.canQueryPackage( |
| Binder.getCallingUid(), packageName)) { |
| // This API can be legitimately called before installation finishes as part of |
| // dex optimization, so we just return false here. |
| return false; |
| } |
| return state.hibernated; |
| } |
| } |
| |
| /** |
| * Set whether the package is hibernating for the given user. |
| * |
| * @param packageName package to modify state |
| * @param userId user |
| * @param isHibernating new hibernation state |
| */ |
| void setHibernatingForUser(String packageName, int userId, boolean isHibernating) { |
| String methodName = "setHibernatingForUser"; |
| if (!sIsServiceEnabled) { |
| return; |
| } |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.MANAGE_APP_HIBERNATION, |
| "Caller does not have MANAGE_APP_HIBERNATION permission."); |
| final int realUserId = handleIncomingUser(userId, methodName); |
| synchronized (mLock) { |
| if (!checkUserStatesExist(realUserId, methodName, /* shouldLog= */ true)) { |
| return; |
| } |
| final Map<String, UserLevelState> packageStates = mUserStates.get(realUserId); |
| final UserLevelState pkgState = packageStates.get(packageName); |
| if (pkgState == null |
| || !mPackageManagerInternal.canQueryPackage( |
| Binder.getCallingUid(), packageName)) { |
| Slog.e(TAG, TextUtils.formatSimple("Package %s is not installed for user %s", |
| packageName, realUserId)); |
| return; |
| } |
| |
| if (pkgState.hibernated == isHibernating) { |
| return; |
| } |
| |
| pkgState.hibernated = isHibernating; |
| if (isHibernating) { |
| mBackgroundExecutor.execute( |
| () -> hibernatePackageForUser(packageName, realUserId, pkgState)); |
| } else { |
| mBackgroundExecutor.execute( |
| () -> unhibernatePackageForUser(packageName, realUserId)); |
| pkgState.lastUnhibernatedMs = System.currentTimeMillis(); |
| } |
| |
| final UserLevelState stateSnapshot = new UserLevelState(pkgState); |
| final int userIdSnapshot = realUserId; |
| mBackgroundExecutor.execute(() -> { |
| FrameworkStatsLog.write( |
| FrameworkStatsLog.USER_LEVEL_HIBERNATION_STATE_CHANGED, |
| stateSnapshot.packageName, |
| userIdSnapshot, |
| stateSnapshot.hibernated); |
| }); |
| List<UserLevelState> states = new ArrayList<>(mUserStates.get(realUserId).values()); |
| mUserDiskStores.get(realUserId).scheduleWriteHibernationStates(states); |
| } |
| } |
| |
| /** |
| * Set whether the package should be hibernated globally at a package level, allowing the |
| * the system to make optimizations at the package or APK level. |
| * |
| * @param packageName package to hibernate globally |
| * @param isHibernating new hibernation state |
| */ |
| void setHibernatingGlobally(String packageName, boolean isHibernating) { |
| if (!sIsServiceEnabled) { |
| return; |
| } |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.MANAGE_APP_HIBERNATION, |
| "Caller does not have MANAGE_APP_HIBERNATION permission."); |
| synchronized (mLock) { |
| GlobalLevelState state = mGlobalHibernationStates.get(packageName); |
| if (state == null |
| || !mPackageManagerInternal.canQueryPackage( |
| Binder.getCallingUid(), packageName)) { |
| Slog.e(TAG, TextUtils.formatSimple( |
| "Package %s is not installed for any user", packageName)); |
| return; |
| } |
| if (state.hibernated != isHibernating) { |
| state.hibernated = isHibernating; |
| if (isHibernating) { |
| mBackgroundExecutor.execute(() -> hibernatePackageGlobally(packageName, state)); |
| } else { |
| state.savedByte = 0; |
| state.lastUnhibernatedMs = System.currentTimeMillis(); |
| } |
| List<GlobalLevelState> states = new ArrayList<>(mGlobalHibernationStates.values()); |
| mGlobalLevelHibernationDiskStore.scheduleWriteHibernationStates(states); |
| } |
| } |
| } |
| |
| /** |
| * Get the hibernating packages for the given user. This is equivalent to the list of |
| * packages for the user that return true for {@link #isHibernatingForUser}. |
| */ |
| @NonNull List<String> getHibernatingPackagesForUser(int userId) { |
| ArrayList<String> hibernatingPackages = new ArrayList<>(); |
| String methodName = "getHibernatingPackagesForUser"; |
| if (!sIsServiceEnabled) { |
| return hibernatingPackages; |
| } |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.MANAGE_APP_HIBERNATION, |
| "Caller does not have MANAGE_APP_HIBERNATION permission."); |
| userId = handleIncomingUser(userId, methodName); |
| synchronized (mLock) { |
| if (!checkUserStatesExist(userId, methodName, /* shouldLog= */ true)) { |
| return hibernatingPackages; |
| } |
| Map<String, UserLevelState> userStates = mUserStates.get(userId); |
| for (UserLevelState state : userStates.values()) { |
| String packageName = state.packageName; |
| if (!mPackageManagerInternal.canQueryPackage( |
| Binder.getCallingUid(), packageName)) { |
| // Package is not visible to caller |
| continue; |
| } |
| if (state.hibernated) { |
| hibernatingPackages.add(state.packageName); |
| } |
| } |
| return hibernatingPackages; |
| } |
| } |
| |
| /** |
| * Return the stats from app hibernation for each package provided. |
| * |
| * @param packageNames the set of packages to return stats for. Returns all if null |
| * @return map from package to stats for that package |
| */ |
| public Map<String, HibernationStats> getHibernationStatsForUser( |
| @Nullable Set<String> packageNames, int userId) { |
| Map<String, HibernationStats> statsMap = new ArrayMap<>(); |
| String methodName = "getHibernationStatsForUser"; |
| if (!sIsServiceEnabled) { |
| return statsMap; |
| } |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.MANAGE_APP_HIBERNATION, |
| "Caller does not have MANAGE_APP_HIBERNATION permission."); |
| userId = handleIncomingUser(userId, methodName); |
| synchronized (mLock) { |
| if (!checkUserStatesExist(userId, methodName, /* shouldLog= */ true)) { |
| return statsMap; |
| } |
| final Map<String, UserLevelState> userPackageStates = mUserStates.get(userId); |
| Set<String> pkgs = packageNames != null ? packageNames : userPackageStates.keySet(); |
| for (String pkgName : pkgs) { |
| if (!mPackageManagerInternal.canQueryPackage(Binder.getCallingUid(), pkgName)) { |
| // Package not visible to caller |
| continue; |
| } |
| if (!mGlobalHibernationStates.containsKey(pkgName) |
| || !userPackageStates.containsKey(pkgName)) { |
| Slog.w(TAG, TextUtils.formatSimple( |
| "No hibernation state associated with package %s user %d. Maybe" |
| + "the package was uninstalled? ", pkgName, userId)); |
| continue; |
| } |
| long diskBytesSaved = mGlobalHibernationStates.get(pkgName).savedByte |
| + userPackageStates.get(pkgName).savedByte; |
| HibernationStats stats = new HibernationStats(diskBytesSaved); |
| statsMap.put(pkgName, stats); |
| } |
| } |
| return statsMap; |
| } |
| |
| /** |
| * Put an app into hibernation for a given user, allowing user-level optimizations to occur. Do |
| * not hold {@link #mLock} while calling this to avoid deadlock scenarios. |
| */ |
| private void hibernatePackageForUser(@NonNull String packageName, int userId, |
| UserLevelState state) { |
| Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "hibernatePackage"); |
| final long caller = Binder.clearCallingIdentity(); |
| try { |
| ApplicationInfo info = mIPackageManager.getApplicationInfo( |
| packageName, PACKAGE_MATCH_FLAGS, userId); |
| StorageStats stats = mStorageStatsManager.queryStatsForPackage( |
| info.storageUuid, packageName, new UserHandle(userId)); |
| mIActivityManager.forceStopPackage(packageName, userId); |
| mIPackageManager.deleteApplicationCacheFilesAsUser(packageName, userId, |
| null /* observer */); |
| synchronized (mLock) { |
| state.savedByte = stats.getCacheBytes(); |
| } |
| } catch (RemoteException e) { |
| throw new IllegalStateException( |
| "Failed to hibernate due to manager not being available", e); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.e(TAG, "Package name not found when querying storage stats", e); |
| } catch (IOException e) { |
| Slog.e(TAG, "Storage device not found", e); |
| } finally { |
| Binder.restoreCallingIdentity(caller); |
| Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); |
| } |
| } |
| |
| /** |
| * Remove a package from hibernation for a given user. Do not hold {@link #mLock} while calling |
| * this. |
| */ |
| private void unhibernatePackageForUser(@NonNull String packageName, int userId) { |
| Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "unhibernatePackage"); |
| final long caller = Binder.clearCallingIdentity(); |
| // Deliver LOCKED_BOOT_COMPLETE AND BOOT_COMPLETE broadcast so app can re-register |
| // their alarms/jobs/etc. |
| try { |
| Intent lockedBcIntent = new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED) |
| .setPackage(packageName); |
| final String[] requiredPermissions = {Manifest.permission.RECEIVE_BOOT_COMPLETED}; |
| mIActivityManager.broadcastIntentWithFeature( |
| null /* caller */, |
| null /* callingFeatureId */, |
| lockedBcIntent, |
| null /* resolvedType */, |
| null /* resultTo */, |
| Activity.RESULT_OK, |
| null /* resultData */, |
| null /* resultExtras */, |
| requiredPermissions, |
| null /* excludedPermissions */, |
| null /* excludedPackages */, |
| OP_NONE, |
| null /* bOptions */, |
| false /* serialized */, |
| false /* sticky */, |
| userId); |
| |
| Intent bcIntent = new Intent(Intent.ACTION_BOOT_COMPLETED).setPackage(packageName); |
| mIActivityManager.broadcastIntentWithFeature( |
| null /* caller */, |
| null /* callingFeatureId */, |
| bcIntent, |
| null /* resolvedType */, |
| null /* resultTo */, |
| Activity.RESULT_OK, |
| null /* resultData */, |
| null /* resultExtras */, |
| requiredPermissions, |
| null /* excludedPermissions */, |
| null /* excludedPackages */, |
| OP_NONE, |
| null /* bOptions */, |
| false /* serialized */, |
| false /* sticky */, |
| userId); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } finally { |
| Binder.restoreCallingIdentity(caller); |
| Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); |
| } |
| } |
| |
| /** |
| * Put a package into global hibernation, optimizing its storage at a package / APK level. Do |
| * not hold {@link #mLock} while calling this. |
| */ |
| private void hibernatePackageGlobally(@NonNull String packageName, GlobalLevelState state) { |
| Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "hibernatePackageGlobally"); |
| long savedBytes = 0; |
| if (mOatArtifactDeletionEnabled) { |
| savedBytes = Math.max( |
| mPackageManagerInternal.deleteOatArtifactsOfPackage(packageName), |
| 0); |
| } |
| synchronized (mLock) { |
| state.savedByte = savedBytes; |
| } |
| Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); |
| } |
| |
| /** |
| * Initializes in-memory store of user-level hibernation states for the given user |
| * |
| * @param userId user id to add installed packages for |
| * @param diskStates states pulled from disk, if available |
| */ |
| @GuardedBy("mLock") |
| private void initializeUserHibernationStates(int userId, |
| @Nullable List<UserLevelState> diskStates) { |
| List<PackageInfo> packages; |
| try { |
| packages = mIPackageManager.getInstalledPackages(PACKAGE_MATCH_FLAGS, userId).getList(); |
| } catch (RemoteException e) { |
| throw new IllegalStateException("Package manager not available", e); |
| } |
| |
| Map<String, UserLevelState> userLevelStates = new ArrayMap<>(); |
| |
| for (int i = 0, size = packages.size(); i < size; i++) { |
| String packageName = packages.get(i).packageName; |
| UserLevelState state = new UserLevelState(); |
| state.packageName = packageName; |
| userLevelStates.put(packageName, state); |
| } |
| |
| if (diskStates != null) { |
| Map<String, PackageInfo> installedPackages = new ArrayMap<>(); |
| for (int i = 0, size = packages.size(); i < size; i++) { |
| installedPackages.put(packages.get(i).packageName, packages.get(i)); |
| } |
| for (int i = 0, size = diskStates.size(); i < size; i++) { |
| String packageName = diskStates.get(i).packageName; |
| PackageInfo pkgInfo = installedPackages.get(packageName); |
| UserLevelState currentState = diskStates.get(i); |
| if (pkgInfo == null) { |
| Slog.w(TAG, TextUtils.formatSimple( |
| "No hibernation state associated with package %s user %d. Maybe" |
| + "the package was uninstalled? ", packageName, userId)); |
| continue; |
| } |
| if (pkgInfo.applicationInfo != null |
| && (pkgInfo.applicationInfo.flags &= ApplicationInfo.FLAG_STOPPED) == 0 |
| && currentState.hibernated) { |
| // App is not stopped but is hibernated. Disk state is stale, so unhibernate |
| // the app. |
| currentState.hibernated = false; |
| currentState.lastUnhibernatedMs = System.currentTimeMillis(); |
| } |
| userLevelStates.put(packageName, currentState); |
| } |
| } |
| mUserStates.put(userId, userLevelStates); |
| } |
| |
| /** |
| * Initialize in-memory store of global level hibernation states. |
| * |
| * @param diskStates global level hibernation states pulled from disk, if available |
| */ |
| @GuardedBy("mLock") |
| private void initializeGlobalHibernationStates(@Nullable List<GlobalLevelState> diskStates) { |
| List<PackageInfo> packages; |
| try { |
| packages = mIPackageManager.getInstalledPackages( |
| PACKAGE_MATCH_FLAGS | MATCH_ANY_USER, 0 /* userId */).getList(); |
| } catch (RemoteException e) { |
| throw new IllegalStateException("Package manager not available", e); |
| } |
| |
| for (int i = 0, size = packages.size(); i < size; i++) { |
| String packageName = packages.get(i).packageName; |
| GlobalLevelState state = new GlobalLevelState(); |
| state.packageName = packageName; |
| mGlobalHibernationStates.put(packageName, state); |
| } |
| if (diskStates != null) { |
| Set<String> installedPackages = new ArraySet<>(); |
| for (int i = 0, size = packages.size(); i < size; i++) { |
| installedPackages.add(packages.get(i).packageName); |
| } |
| for (int i = 0, size = diskStates.size(); i < size; i++) { |
| GlobalLevelState state = diskStates.get(i); |
| if (!installedPackages.contains(state.packageName)) { |
| Slog.w(TAG, TextUtils.formatSimple( |
| "No hibernation state associated with package %s. Maybe the " |
| + "package was uninstalled? ", state.packageName)); |
| continue; |
| } |
| mGlobalHibernationStates.put(state.packageName, state); |
| } |
| } |
| } |
| |
| @Override |
| public void onUserUnlocking(@NonNull TargetUser user) { |
| int userId = user.getUserIdentifier(); |
| HibernationStateDiskStore<UserLevelState> diskStore = |
| mInjector.getUserLevelDiskStore(userId); |
| mUserDiskStores.put(userId, diskStore); |
| mBackgroundExecutor.execute(() -> { |
| List<UserLevelState> storedStates = diskStore.readHibernationStates(); |
| synchronized (mLock) { |
| // Ensure user hasn't stopped in the time to execute. |
| if (mUserManager.isUserUnlockingOrUnlocked(userId)) { |
| initializeUserHibernationStates(userId, storedStates); |
| // Globally unhibernate a package if the unlocked user does not have it |
| // hibernated. |
| for (UserLevelState userState : mUserStates.get(userId).values()) { |
| String pkgName = userState.packageName; |
| GlobalLevelState globalState = mGlobalHibernationStates.get(pkgName); |
| if (globalState.hibernated && !userState.hibernated) { |
| setHibernatingGlobally(pkgName, false); |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void onUserStopping(@NonNull TargetUser user) { |
| int userId = user.getUserIdentifier(); |
| // TODO: Flush any scheduled writes to disk immediately on user stopping / power off. |
| synchronized (mLock) { |
| mUserDiskStores.remove(userId); |
| mUserStates.remove(userId); |
| } |
| } |
| |
| private void onPackageAdded(@NonNull String packageName, int userId) { |
| synchronized (mLock) { |
| if (!mUserStates.contains(userId)) { |
| return; |
| } |
| UserLevelState userState = new UserLevelState(); |
| userState.packageName = packageName; |
| mUserStates.get(userId).put(packageName, userState); |
| if (!mGlobalHibernationStates.containsKey(packageName)) { |
| GlobalLevelState globalState = new GlobalLevelState(); |
| globalState.packageName = packageName; |
| mGlobalHibernationStates.put(packageName, globalState); |
| } |
| } |
| } |
| |
| private void onPackageRemoved(@NonNull String packageName, int userId) { |
| synchronized (mLock) { |
| if (!mUserStates.contains(userId)) { |
| return; |
| } |
| mUserStates.get(userId).remove(packageName); |
| } |
| } |
| |
| private void onPackageRemovedForAllUsers(@NonNull String packageName) { |
| synchronized (mLock) { |
| mGlobalHibernationStates.remove(packageName); |
| } |
| } |
| |
| private void onDeviceConfigChanged(Properties properties) { |
| for (String key : properties.getKeyset()) { |
| if (TextUtils.equals(KEY_APP_HIBERNATION_ENABLED, key)) { |
| sIsServiceEnabled = isDeviceConfigAppHibernationEnabled(); |
| Slog.d(TAG, "App hibernation changed to enabled=" + sIsServiceEnabled); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| } |
| |
| /** |
| * Check that user states exist. |
| * |
| * @param userId user to check |
| * @param methodName method name that is calling. Used for logging purposes. |
| * @param shouldLog whether we should log why the user state doesn't exist |
| * @return true if user states exist |
| */ |
| @GuardedBy("mLock") |
| private boolean checkUserStatesExist(int userId, String methodName, boolean shouldLog) { |
| if (!mUserManager.isUserUnlockingOrUnlocked(userId)) { |
| if (shouldLog) { |
| Slog.w(TAG, TextUtils.formatSimple( |
| "Attempt to call %s on stopped or nonexistent user %d", |
| methodName, userId)); |
| } |
| return false; |
| } |
| if (!mUserStates.contains(userId)) { |
| if (shouldLog) { |
| Slog.w(TAG, TextUtils.formatSimple( |
| "Attempt to call %s before states have been read from disk", methodName)); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| private void dump(PrintWriter pw) { |
| // Check usage stats permission since hibernation indirectly informs usage. |
| if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return; |
| |
| IndentingPrintWriter idpw = new IndentingPrintWriter(pw, " "); |
| |
| synchronized (mLock) { |
| final int userCount = mUserStates.size(); |
| for (int i = 0; i < userCount; i++) { |
| final int userId = mUserStates.keyAt(i); |
| idpw.print("User Level Hibernation States, "); |
| idpw.printPair("user", userId); |
| idpw.println(); |
| Map<String, UserLevelState> stateMap = mUserStates.get(userId); |
| idpw.increaseIndent(); |
| for (UserLevelState state : stateMap.values()) { |
| idpw.print(state); |
| idpw.println(); |
| } |
| idpw.decreaseIndent(); |
| } |
| idpw.println(); |
| idpw.print("Global Level Hibernation States"); |
| idpw.println(); |
| for (GlobalLevelState state : mGlobalHibernationStates.values()) { |
| idpw.print(state); |
| idpw.println(); |
| } |
| } |
| } |
| |
| private final AppHibernationManagerInternal mLocalService = new LocalService(this); |
| |
| private static final class LocalService extends AppHibernationManagerInternal { |
| private final AppHibernationService mService; |
| |
| LocalService(AppHibernationService service) { |
| mService = service; |
| } |
| |
| @Override |
| public boolean isHibernatingForUser(String packageName, int userId) { |
| return mService.isHibernatingForUser(packageName, userId); |
| } |
| |
| @Override |
| public void setHibernatingForUser(String packageName, int userId, boolean isHibernating) { |
| mService.setHibernatingForUser(packageName, userId, isHibernating); |
| } |
| |
| @Override |
| public void setHibernatingGlobally(String packageName, boolean isHibernating) { |
| mService.setHibernatingGlobally(packageName, isHibernating); |
| } |
| |
| @Override |
| public boolean isHibernatingGlobally(String packageName) { |
| return mService.isHibernatingGlobally(packageName); |
| } |
| |
| @Override |
| public boolean isOatArtifactDeletionEnabled() { |
| return mService.isOatArtifactDeletionEnabled(); |
| } |
| } |
| |
| private final AppHibernationServiceStub mServiceStub = new AppHibernationServiceStub(this); |
| |
| static final class AppHibernationServiceStub extends IAppHibernationService.Stub { |
| final AppHibernationService mService; |
| |
| AppHibernationServiceStub(AppHibernationService service) { |
| mService = service; |
| } |
| |
| @Override |
| public boolean isHibernatingForUser(String packageName, int userId) { |
| return mService.isHibernatingForUser(packageName, userId); |
| } |
| |
| @Override |
| public void setHibernatingForUser(String packageName, int userId, boolean isHibernating) { |
| mService.setHibernatingForUser(packageName, userId, isHibernating); |
| } |
| |
| @Override |
| public void setHibernatingGlobally(String packageName, boolean isHibernating) { |
| mService.setHibernatingGlobally(packageName, isHibernating); |
| } |
| |
| @Override |
| public boolean isHibernatingGlobally(String packageName) { |
| return mService.isHibernatingGlobally(packageName); |
| } |
| |
| @Override |
| public List<String> getHibernatingPackagesForUser(int userId) { |
| return mService.getHibernatingPackagesForUser(userId); |
| } |
| |
| @Override |
| public Map<String, HibernationStats> getHibernationStatsForUser( |
| @Nullable List<String> packageNames, int userId) { |
| Set<String> pkgsSet = packageNames != null ? new ArraySet<>(packageNames) : null; |
| return mService.getHibernationStatsForUser(pkgsSet, userId); |
| } |
| |
| @Override |
| public boolean isOatArtifactDeletionEnabled() { |
| return mService.isOatArtifactDeletionEnabled(); |
| } |
| |
| @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); |
| } |
| |
| @Override |
| protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, |
| @Nullable String[] args) { |
| mService.dump(fout); |
| } |
| } |
| |
| // Broadcast receiver for 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_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); |
| if (intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false)) { |
| onPackageRemovedForAllUsers(packageName); |
| } |
| } |
| } |
| } |
| }; |
| |
| private final UsageEventListener mUsageEventListener = (userId, event) -> { |
| if (!isAppHibernationEnabled()) { |
| return; |
| } |
| final int eventType = event.mEventType; |
| if (eventType == UsageEvents.Event.USER_INTERACTION |
| || eventType == UsageEvents.Event.ACTIVITY_RESUMED |
| || eventType == UsageEvents.Event.APP_COMPONENT_USED) { |
| final String pkgName = event.mPackage; |
| setHibernatingForUser(pkgName, userId, false); |
| setHibernatingGlobally(pkgName, false); |
| } |
| }; |
| |
| /** |
| * Whether app hibernation is enabled on this device. |
| * |
| * @return true if enabled, false otherwise |
| */ |
| public static boolean isAppHibernationEnabled() { |
| return sIsServiceEnabled; |
| } |
| |
| private static boolean isDeviceConfigAppHibernationEnabled() { |
| return DeviceConfig.getBoolean( |
| NAMESPACE_APP_HIBERNATION, |
| KEY_APP_HIBERNATION_ENABLED, |
| true /* defaultValue */); |
| } |
| |
| /** |
| * Dependency injector for {@link #AppHibernationService)}. |
| */ |
| interface Injector { |
| Context getContext(); |
| |
| IPackageManager getPackageManager(); |
| |
| PackageManagerInternal getPackageManagerInternal(); |
| |
| IActivityManager getActivityManager(); |
| |
| UserManager getUserManager(); |
| |
| StorageStatsManager getStorageStatsManager(); |
| |
| Executor getBackgroundExecutor(); |
| |
| UsageStatsManagerInternal getUsageStatsManagerInternal(); |
| |
| HibernationStateDiskStore<GlobalLevelState> getGlobalLevelDiskStore(); |
| |
| HibernationStateDiskStore<UserLevelState> getUserLevelDiskStore(int userId); |
| |
| boolean isOatArtifactDeletionEnabled(); |
| } |
| |
| private static final class InjectorImpl implements Injector { |
| private static final String HIBERNATION_DIR_NAME = "hibernation"; |
| private final Context mContext; |
| private final ScheduledExecutorService mScheduledExecutorService; |
| private final UserLevelHibernationProto mUserLevelHibernationProto; |
| |
| InjectorImpl(Context context) { |
| mContext = context; |
| mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); |
| mUserLevelHibernationProto = new UserLevelHibernationProto(); |
| } |
| |
| @Override |
| public Context getContext() { |
| return mContext; |
| } |
| |
| @Override |
| public IPackageManager getPackageManager() { |
| return IPackageManager.Stub.asInterface(ServiceManager.getService("package")); |
| } |
| |
| @Override |
| public PackageManagerInternal getPackageManagerInternal() { |
| return LocalServices.getService(PackageManagerInternal.class); |
| } |
| |
| @Override |
| public IActivityManager getActivityManager() { |
| return ActivityManager.getService(); |
| } |
| |
| @Override |
| public UserManager getUserManager() { |
| return mContext.getSystemService(UserManager.class); |
| } |
| |
| @Override |
| public StorageStatsManager getStorageStatsManager() { |
| return mContext.getSystemService(StorageStatsManager.class); |
| } |
| |
| @Override |
| public Executor getBackgroundExecutor() { |
| return mScheduledExecutorService; |
| } |
| |
| @Override |
| public UsageStatsManagerInternal getUsageStatsManagerInternal() { |
| return LocalServices.getService(UsageStatsManagerInternal.class); |
| } |
| |
| @Override |
| public HibernationStateDiskStore<GlobalLevelState> getGlobalLevelDiskStore() { |
| File dir = new File(Environment.getDataSystemDirectory(), HIBERNATION_DIR_NAME); |
| return new HibernationStateDiskStore<>( |
| dir, new GlobalLevelHibernationProto(), mScheduledExecutorService); |
| } |
| |
| @Override |
| public HibernationStateDiskStore<UserLevelState> getUserLevelDiskStore(int userId) { |
| File dir = new File(Environment.getDataSystemCeDirectory(userId), HIBERNATION_DIR_NAME); |
| return new HibernationStateDiskStore<>( |
| dir, mUserLevelHibernationProto, mScheduledExecutorService); |
| } |
| |
| @Override |
| public boolean isOatArtifactDeletionEnabled() { |
| return mContext.getResources().getBoolean( |
| com.android.internal.R.bool.config_hibernationDeletesOatArtifactsEnabled); |
| } |
| } |
| |
| private final class StatsPullAtomCallbackImpl implements StatsPullAtomCallback { |
| |
| private static final int MEGABYTE_IN_BYTES = 1000000; |
| |
| @Override |
| public int onPullAtom(int atomTag, @NonNull List<StatsEvent> data) { |
| if (!isAppHibernationEnabled() |
| && (atomTag == FrameworkStatsLog.USER_LEVEL_HIBERNATED_APPS |
| || atomTag == FrameworkStatsLog.GLOBAL_HIBERNATED_APPS)) { |
| return StatsManager.PULL_SUCCESS; |
| } |
| |
| switch (atomTag) { |
| case FrameworkStatsLog.USER_LEVEL_HIBERNATED_APPS: |
| List<UserInfo> userInfos = mUserManager.getAliveUsers(); |
| final int numUsers = userInfos.size(); |
| for (int i = 0; i < numUsers; ++i) { |
| final int userId = userInfos.get(i).id; |
| if (mUserManager.isUserUnlockingOrUnlocked(userId)) { |
| data.add( |
| FrameworkStatsLog.buildStatsEvent( |
| atomTag, |
| getHibernatingPackagesForUser(userId).size(), |
| userId) |
| ); |
| } |
| } |
| break; |
| case FrameworkStatsLog.GLOBAL_HIBERNATED_APPS: |
| int hibernatedAppCount = 0; |
| long storage_saved_byte = 0; |
| synchronized (mLock) { |
| for (GlobalLevelState state : mGlobalHibernationStates.values()) { |
| if (state.hibernated) { |
| hibernatedAppCount++; |
| storage_saved_byte += state.savedByte; |
| } |
| } |
| } |
| data.add( |
| FrameworkStatsLog.buildStatsEvent( |
| atomTag, |
| hibernatedAppCount, |
| storage_saved_byte / MEGABYTE_IN_BYTES) |
| ); |
| break; |
| default: |
| return StatsManager.PULL_SKIP; |
| } |
| return StatsManager.PULL_SUCCESS; |
| } |
| } |
| } |