Add persistence for app hibernation states

Persist hibernation states to disk. This CL persists the user-level
and global hibernation states to disk.

Bug: 175829330
Test: atest AppHibernationServiceTest
Test: atest HibernationStateDiskStoreTest
Change-Id: If58d648d720bed1693b9346c4d0e85074daec931
This commit is contained in:
Kevin Han
2021-01-28 14:13:10 -08:00
parent 6669dec038
commit dba83c49dd
11 changed files with 888 additions and 43 deletions

View File

@@ -16,6 +16,7 @@ ogunwale@google.com
jjaggi@google.com
roosa@google.com
per-file usagestatsservice.proto, usagestatsservice_v2.proto = mwachens@google.com
per-file apphibernationservice.proto = file:/core/java/android/apphibernation/OWNERS
# Biometrics
kchyn@google.com

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
syntax = "proto2";
package com.android.server.apphibernation;
option java_multiple_files = true;
// Proto for hibernation states for all packages for a user.
message UserLevelHibernationStatesProto {
repeated UserLevelHibernationStateProto hibernation_state = 1;
}
// Proto for com.android.server.apphibernation.UserLevelState.
message UserLevelHibernationStateProto {
optional string package_name = 1;
optional bool hibernated = 2;
}
// Proto for global hibernation states for all packages.
message GlobalLevelHibernationStatesProto {
repeated GlobalLevelHibernationStateProto hibernation_state = 1;
}
// Proto for com.android.server.apphibernation.GlobalLevelState
message GlobalLevelHibernationStateProto {
optional string package_name = 1;
optional bool hibernated = 2;
}

View File

@@ -20,7 +20,7 @@ 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_ALL;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
import android.annotation.NonNull;
@@ -34,7 +34,9 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Environment;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
@@ -52,10 +54,14 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
import java.io.File;
import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
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
@@ -64,6 +70,11 @@ import java.util.Set;
*/
public final class AppHibernationService extends SystemService {
private static final String TAG = "AppHibernationService";
private static final int PACKAGE_MATCH_FLAGS =
PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_DISABLED_COMPONENTS;
/**
* Lock for accessing any in-memory hibernation state
@@ -74,9 +85,13 @@ public final class AppHibernationService extends SystemService {
private final IActivityManager mIActivityManager;
private final UserManager mUserManager;
@GuardedBy("mLock")
private final SparseArray<Map<String, UserPackageState>> mUserStates = new SparseArray<>();
private final SparseArray<Map<String, UserLevelState>> mUserStates = new SparseArray<>();
private final SparseArray<HibernationStateDiskStore<UserLevelState>> mUserDiskStores =
new SparseArray<>();
@GuardedBy("mLock")
private final Set<String> mGloballyHibernatedPackages = new ArraySet<>();
private final Map<String, GlobalLevelState> mGlobalHibernationStates = new ArrayMap<>();
private final HibernationStateDiskStore<GlobalLevelState> mGlobalLevelHibernationDiskStore;
private final Injector mInjector;
/**
* Initializes the system service.
@@ -98,6 +113,8 @@ public final class AppHibernationService extends SystemService {
mIPackageManager = injector.getPackageManager();
mIActivityManager = injector.getActivityManager();
mUserManager = injector.getUserManager();
mGlobalLevelHibernationDiskStore = injector.getGlobalLevelDiskStore();
mInjector = injector;
final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */);
@@ -113,6 +130,17 @@ public final class AppHibernationService extends SystemService {
publishBinderService(Context.APP_HIBERNATION_SERVICE, mServiceStub);
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_BOOT_COMPLETED) {
List<GlobalLevelState> states =
mGlobalLevelHibernationDiskStore.readHibernationStates();
synchronized (mLock) {
initializeGlobalHibernationStates(states);
}
}
}
/**
* Whether a package is hibernating for a given user.
*
@@ -128,8 +156,8 @@ public final class AppHibernationService extends SystemService {
return false;
}
synchronized (mLock) {
final Map<String, UserPackageState> packageStates = mUserStates.get(userId);
final UserPackageState pkgState = packageStates.get(packageName);
final Map<String, UserLevelState> packageStates = mUserStates.get(userId);
final UserLevelState pkgState = packageStates.get(packageName);
if (pkgState == null) {
throw new IllegalArgumentException(
String.format("Package %s is not installed for user %s",
@@ -147,7 +175,12 @@ public final class AppHibernationService extends SystemService {
*/
boolean isHibernatingGlobally(String packageName) {
synchronized (mLock) {
return mGloballyHibernatedPackages.contains(packageName);
GlobalLevelState state = mGlobalHibernationStates.get(packageName);
if (state == null) {
throw new IllegalArgumentException(
String.format("Package %s is not installed", packageName));
}
return state.hibernated;
}
}
@@ -166,8 +199,8 @@ public final class AppHibernationService extends SystemService {
return;
}
synchronized (mLock) {
Map<String, UserPackageState> packageStates = mUserStates.get(userId);
UserPackageState pkgState = packageStates.get(packageName);
final Map<String, UserLevelState> packageStates = mUserStates.get(userId);
final UserLevelState pkgState = packageStates.get(packageName);
if (pkgState == null) {
throw new IllegalArgumentException(
String.format("Package %s is not installed for user %s",
@@ -183,6 +216,8 @@ public final class AppHibernationService extends SystemService {
} else {
unhibernatePackageForUser(packageName, userId, pkgState);
}
List<UserLevelState> states = new ArrayList<>(mUserStates.get(userId).values());
mUserDiskStores.get(userId).scheduleWriteHibernationStates(states);
}
}
@@ -194,13 +229,20 @@ public final class AppHibernationService extends SystemService {
* @param isHibernating new hibernation state
*/
void setHibernatingGlobally(String packageName, boolean isHibernating) {
if (isHibernating != mGloballyHibernatedPackages.contains(packageName)) {
synchronized (mLock) {
synchronized (mLock) {
GlobalLevelState state = mGlobalHibernationStates.get(packageName);
if (state == null) {
throw new IllegalArgumentException(
String.format("Package %s is not installed for any user", packageName));
}
if (state.hibernated != isHibernating) {
if (isHibernating) {
hibernatePackageGlobally(packageName);
hibernatePackageGlobally(packageName, state);
} else {
unhibernatePackageGlobally(packageName);
unhibernatePackageGlobally(packageName, state);
}
List<GlobalLevelState> states = new ArrayList<>(mGlobalHibernationStates.values());
mGlobalLevelHibernationDiskStore.scheduleWriteHibernationStates(states);
}
}
}
@@ -212,7 +254,7 @@ public final class AppHibernationService extends SystemService {
*/
@GuardedBy("mLock")
private void hibernatePackageForUser(@NonNull String packageName, int userId,
@NonNull UserPackageState pkgState) {
@NonNull UserLevelState pkgState) {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "hibernatePackage");
final long caller = Binder.clearCallingIdentity();
try {
@@ -236,7 +278,7 @@ public final class AppHibernationService extends SystemService {
*/
@GuardedBy("mLock")
private void unhibernatePackageForUser(@NonNull String packageName, int userId,
UserPackageState pkgState) {
UserLevelState pkgState) {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "unhibernatePackage");
final long caller = Binder.clearCallingIdentity();
try {
@@ -255,10 +297,10 @@ public final class AppHibernationService extends SystemService {
* Put a package into global hibernation, optimizing its storage at a package / APK level.
*/
@GuardedBy("mLock")
private void hibernatePackageGlobally(@NonNull String packageName) {
private void hibernatePackageGlobally(@NonNull String packageName, GlobalLevelState state) {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "hibernatePackageGlobally");
// TODO(175830194): Delete vdex/odex when DexManager API is built out
mGloballyHibernatedPackages.add(packageName);
state.hibernated = true;
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
@@ -266,52 +308,127 @@ public final class AppHibernationService extends SystemService {
* Unhibernate a package from global hibernation.
*/
@GuardedBy("mLock")
private void unhibernatePackageGlobally(@NonNull String packageName) {
private void unhibernatePackageGlobally(@NonNull String packageName, GlobalLevelState state) {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "unhibernatePackageGlobally");
mGloballyHibernatedPackages.remove(packageName);
state.hibernated = false;
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
/**
* Populates {@link #mUserStates} with the users installed packages.
* 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 addUserPackageStates(int userId) {
Map<String, UserPackageState> packages = new ArrayMap<>();
List<PackageInfo> packageList;
private void initializeUserHibernationStates(int userId,
@Nullable List<UserLevelState> diskStates) {
List<PackageInfo> packages;
try {
packageList = mIPackageManager.getInstalledPackages(MATCH_ALL, userId).getList();
packages = mIPackageManager.getInstalledPackages(PACKAGE_MATCH_FLAGS, userId).getList();
} catch (RemoteException e) {
throw new IllegalStateException("Package manager not available.", e);
throw new IllegalStateException("Package manager not available", e);
}
for (int i = 0, size = packageList.size(); i < size; i++) {
packages.put(packageList.get(i).packageName, new UserPackageState());
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) {
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++) {
String packageName = diskStates.get(i).packageName;
if (!installedPackages.contains(packageName)) {
Slog.w(TAG, String.format(
"No hibernation state associated with package %s user %d. Maybe"
+ "the package was uninstalled? ", packageName, userId));
continue;
}
userLevelStates.put(packageName, diskStates.get(i));
}
}
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, String.format(
"No hibernation state associated with package %s. Maybe the "
+ "package was uninstalled? ", state.packageName));
continue;
}
mGlobalHibernationStates.put(state.packageName, state);
}
}
mUserStates.put(userId, packages);
}
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
// TODO: Pull from persistent disk storage. For now, just make from scratch.
int userId = user.getUserIdentifier();
HibernationStateDiskStore<UserLevelState> diskStore =
mInjector.getUserLevelDiskStore(userId);
mUserDiskStores.put(userId, diskStore);
List<UserLevelState> storedStates = diskStore.readHibernationStates();
synchronized (mLock) {
addUserPackageStates(user.getUserIdentifier());
initializeUserHibernationStates(userId, storedStates);
}
}
@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) {
// TODO: Flush to disk when persistence is implemented
mUserStates.remove(user.getUserIdentifier());
mUserDiskStores.remove(userId);
mUserStates.remove(userId);
}
}
private void onPackageAdded(@NonNull String packageName, int userId) {
synchronized (mLock) {
mUserStates.get(userId).put(packageName, new UserPackageState());
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);
}
}
}
@@ -323,7 +440,7 @@ public final class AppHibernationService extends SystemService {
private void onPackageRemovedForAllUsers(@NonNull String packageName) {
synchronized (mLock) {
mGloballyHibernatedPackages.remove(packageName);
mGlobalHibernationStates.remove(packageName);
}
}
@@ -423,14 +540,6 @@ public final class AppHibernationService extends SystemService {
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
}
/**
* Dependency injector for {@link #AppHibernationService)}.
*/
@@ -442,13 +551,22 @@ public final class AppHibernationService extends SystemService {
IActivityManager getActivityManager();
UserManager getUserManager();
HibernationStateDiskStore<GlobalLevelState> getGlobalLevelDiskStore();
HibernationStateDiskStore<UserLevelState> getUserLevelDiskStore(int userId);
}
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
@@ -470,5 +588,19 @@ public final class AppHibernationService extends SystemService {
public UserManager getUserManager() {
return mContext.getSystemService(UserManager.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);
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Reads and writes protos for {@link GlobalLevelState} hiberation states.
*/
final class GlobalLevelHibernationProto implements ProtoReadWriter<List<GlobalLevelState>> {
private static final String TAG = "GlobalLevelHibernationProtoReadWriter";
@Override
public void writeToProto(@NonNull ProtoOutputStream stream,
@NonNull List<GlobalLevelState> data) {
for (int i = 0, size = data.size(); i < size; i++) {
long token = stream.start(GlobalLevelHibernationStatesProto.HIBERNATION_STATE);
GlobalLevelState state = data.get(i);
stream.write(GlobalLevelHibernationStateProto.PACKAGE_NAME, state.packageName);
stream.write(GlobalLevelHibernationStateProto.HIBERNATED, state.hibernated);
stream.end(token);
}
}
@Override
public @Nullable List<GlobalLevelState> readFromProto(@NonNull ProtoInputStream stream)
throws IOException {
List<GlobalLevelState> list = new ArrayList<>();
while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
if (stream.getFieldNumber()
!= (int) GlobalLevelHibernationStatesProto.HIBERNATION_STATE) {
continue;
}
GlobalLevelState state = new GlobalLevelState();
long token = stream.start(GlobalLevelHibernationStatesProto.HIBERNATION_STATE);
while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (stream.getFieldNumber()) {
case (int) GlobalLevelHibernationStateProto.PACKAGE_NAME:
state.packageName =
stream.readString(GlobalLevelHibernationStateProto.PACKAGE_NAME);
break;
case (int) GlobalLevelHibernationStateProto.HIBERNATED:
state.hibernated =
stream.readBoolean(GlobalLevelHibernationStateProto.HIBERNATED);
break;
default:
Slog.w(TAG, "Undefined field in proto: " + stream.getFieldNumber());
}
}
stream.end(token);
list.add(state);
}
return list;
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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;
/**
* Data class that contains global hibernation state for a package.
*/
final class GlobalLevelState {
public String packageName;
public boolean hibernated;
}

View File

@@ -0,0 +1,162 @@
/*
* 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.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.text.format.DateUtils;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Disk store utility class for hibernation states.
*
* @param <T> the type of hibernation state data
*/
class HibernationStateDiskStore<T> {
private static final String TAG = "HibernationStateDiskStore";
// Time to wait before actually writing. Saves extra writes if data changes come in batches.
private static final long DISK_WRITE_DELAY = 1L * DateUtils.MINUTE_IN_MILLIS;
private static final String STATES_FILE_NAME = "states";
private final File mHibernationFile;
private final ScheduledExecutorService mExecutorService;
private final ProtoReadWriter<List<T>> mProtoReadWriter;
private List<T> mScheduledStatesToWrite = new ArrayList<>();
private ScheduledFuture<?> mFuture;
/**
* Initialize a disk store for hibernation states in the given directory.
*
* @param hibernationDir directory to write/read states file
* @param readWriter writer/reader of states proto
* @param executorService scheduled executor for writing data
*/
HibernationStateDiskStore(@NonNull File hibernationDir,
@NonNull ProtoReadWriter<List<T>> readWriter,
@NonNull ScheduledExecutorService executorService) {
this(hibernationDir, readWriter, executorService, STATES_FILE_NAME);
}
@VisibleForTesting
HibernationStateDiskStore(@NonNull File hibernationDir,
@NonNull ProtoReadWriter<List<T>> readWriter,
@NonNull ScheduledExecutorService executorService,
@NonNull String fileName) {
mHibernationFile = new File(hibernationDir, fileName);
mExecutorService = executorService;
mProtoReadWriter = readWriter;
}
/**
* Schedule a full write of all the hibernation states to the file on disk. Does not run
* immediately and subsequent writes override previous ones.
*
* @param hibernationStates list of hibernation states to write to disk
*/
void scheduleWriteHibernationStates(@NonNull List<T> hibernationStates) {
synchronized (this) {
mScheduledStatesToWrite = hibernationStates;
if (mExecutorService.isShutdown()) {
Slog.e(TAG, "Scheduled executor service is shut down.");
return;
}
// Already have write scheduled
if (mFuture != null) {
Slog.i(TAG, "Write already scheduled. Skipping schedule.");
return;
}
mFuture = mExecutorService.schedule(this::writeHibernationStates, DISK_WRITE_DELAY,
TimeUnit.MILLISECONDS);
}
}
/**
* Read hibernation states from disk.
*
* @return the parsed list of hibernation states, null if file does not exist
*/
@Nullable
List<T> readHibernationStates() {
synchronized (this) {
if (!mHibernationFile.exists()) {
Slog.i(TAG, "No hibernation file on disk for file " + mHibernationFile.getPath());
return null;
}
AtomicFile atomicFile = new AtomicFile(mHibernationFile);
try {
FileInputStream inputStream = atomicFile.openRead();
ProtoInputStream protoInputStream = new ProtoInputStream(inputStream);
return mProtoReadWriter.readFromProto(protoInputStream);
} catch (IOException e) {
Slog.e(TAG, "Failed to read states protobuf.", e);
return null;
}
}
}
@WorkerThread
private void writeHibernationStates() {
synchronized (this) {
writeStateProto(mScheduledStatesToWrite);
mScheduledStatesToWrite.clear();
mFuture = null;
}
}
@WorkerThread
private void writeStateProto(List<T> states) {
AtomicFile atomicFile = new AtomicFile(mHibernationFile);
FileOutputStream fileOutputStream;
try {
fileOutputStream = atomicFile.startWrite();
} catch (IOException e) {
Slog.e(TAG, "Failed to start write to states protobuf.", e);
return;
}
try {
ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
mProtoReadWriter.writeToProto(protoOutputStream, states);
protoOutputStream.flush();
atomicFile.finishWrite(fileOutputStream);
} catch (Exception e) {
Slog.e(TAG, "Failed to finish write to states protobuf.", e);
atomicFile.failWrite(fileOutputStream);
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.annotation.NonNull;
import android.annotation.Nullable;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import java.io.IOException;
/**
* Proto utility that reads and writes proto for some data.
*
* @param <T> data that can be written and read from a proto
*/
interface ProtoReadWriter<T> {
/**
* Write data to a proto stream
*/
void writeToProto(@NonNull ProtoOutputStream stream, @NonNull T data);
/**
* Parse data from the proto stream and return
*/
@Nullable T readFromProto(@NonNull ProtoInputStream stream) throws IOException;
}

View File

@@ -0,0 +1,78 @@
/*
* 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.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Reads and writes protos for {@link UserLevelState} hiberation states.
*/
final class UserLevelHibernationProto implements ProtoReadWriter<List<UserLevelState>> {
private static final String TAG = "UserLevelHibernationProtoReadWriter";
@Override
public void writeToProto(@NonNull ProtoOutputStream stream,
@NonNull List<UserLevelState> data) {
for (int i = 0, size = data.size(); i < size; i++) {
long token = stream.start(UserLevelHibernationStatesProto.HIBERNATION_STATE);
UserLevelState state = data.get(i);
stream.write(UserLevelHibernationStateProto.PACKAGE_NAME, state.packageName);
stream.write(UserLevelHibernationStateProto.HIBERNATED, state.hibernated);
stream.end(token);
}
}
@Override
public @Nullable List<UserLevelState> readFromProto(@NonNull ProtoInputStream stream)
throws IOException {
List<UserLevelState> list = new ArrayList<>();
while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
if (stream.getFieldNumber()
!= (int) UserLevelHibernationStatesProto.HIBERNATION_STATE) {
continue;
}
UserLevelState state = new UserLevelState();
long token = stream.start(UserLevelHibernationStatesProto.HIBERNATION_STATE);
while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (stream.getFieldNumber()) {
case (int) UserLevelHibernationStateProto.PACKAGE_NAME:
state.packageName =
stream.readString(UserLevelHibernationStateProto.PACKAGE_NAME);
break;
case (int) UserLevelHibernationStateProto.HIBERNATED:
state.hibernated =
stream.readBoolean(UserLevelHibernationStateProto.HIBERNATED);
break;
default:
Slog.w(TAG, "Undefined field in proto: " + stream.getFieldNumber());
}
}
stream.end(token);
list.add(state);
}
return list;
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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;
/**
* Data class that contains hibernation state info of a package for a user.
*/
final class UserLevelState {
public String packageName;
public boolean hibernated;
}

View File

@@ -16,12 +16,15 @@
package com.android.server.apphibernation;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
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.ArgumentMatchers.intThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
@@ -47,6 +50,7 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
@@ -75,11 +79,15 @@ public final class AppHibernationServiceTest {
private IActivityManager mIActivityManager;
@Mock
private UserManager mUserManager;
@Mock
private HibernationStateDiskStore<UserLevelState> mHibernationStateDiskStore;
@Captor
private ArgumentCaptor<BroadcastReceiver> mReceiverCaptor;
@Before
public void setUp() throws RemoteException {
// Share class loader to allow access to package-private classes
System.setProperty("dexmaker.share_classloader", "true");
MockitoAnnotations.initMocks(this);
doReturn(mContext).when(mContext).createContextAsUser(any(), anyInt());
@@ -93,6 +101,12 @@ public final class AppHibernationServiceTest {
doAnswer(returnsArgAt(2)).when(mIActivityManager).handleIncomingUser(anyInt(), anyInt(),
anyInt(), anyBoolean(), anyBoolean(), any(), any());
List<PackageInfo> packages = new ArrayList<>();
packages.add(makePackageInfo(PACKAGE_NAME_1));
doReturn(new ParceledListSlice<>(packages)).when(mIPackageManager).getInstalledPackages(
intThat(arg -> (arg & MATCH_ANY_USER) != 0), anyInt());
mAppHibernationService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
UserInfo userInfo = addUser(USER_ID_1);
mAppHibernationService.onUserUnlocking(new SystemService.TargetUser(userInfo));
doReturn(true).when(mUserManager).isUserUnlockingOrUnlocked(USER_ID_1);
@@ -151,7 +165,7 @@ public final class AppHibernationServiceTest {
}
@Test
public void testSetHibernatingGlobally_packageIsHibernatingGlobally() {
public void testSetHibernatingGlobally_packageIsHibernatingGlobally() throws RemoteException {
// WHEN we hibernate a package
mAppHibernationService.setHibernatingGlobally(PACKAGE_NAME_1, true);
@@ -177,7 +191,7 @@ public final class AppHibernationServiceTest {
userPackages.add(makePackageInfo(pkgName));
}
doReturn(new ParceledListSlice<>(userPackages)).when(mIPackageManager)
.getInstalledPackages(anyInt(), eq(userId));
.getInstalledPackages(intThat(arg -> (arg & MATCH_ANY_USER) == 0), eq(userId));
return userInfo;
}
@@ -213,5 +227,15 @@ public final class AppHibernationServiceTest {
public UserManager getUserManager() {
return mUserManager;
}
@Override
public HibernationStateDiskStore<GlobalLevelState> getGlobalLevelDiskStore() {
return Mockito.mock(HibernationStateDiskStore.class);
}
@Override
public HibernationStateDiskStore<UserLevelState> getUserLevelDiskStore(int userId) {
return Mockito.mock(HibernationStateDiskStore.class);
}
}
}

View File

@@ -0,0 +1,236 @@
/*
* 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.assertEquals;
import android.os.FileUtils;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@SmallTest
public class HibernationStateDiskStoreTest {
private static final String STATES_FILE_NAME = "states";
private final MockScheduledExecutorService mMockScheduledExecutorService =
new MockScheduledExecutorService();
private File mFile;
private HibernationStateDiskStore<String> mHibernationStateDiskStore;
@Before
public void setUp() {
mFile = new File(InstrumentationRegistry.getContext().getCacheDir(), "test");
mHibernationStateDiskStore = new HibernationStateDiskStore<>(mFile,
new MockProtoReadWriter(), mMockScheduledExecutorService, STATES_FILE_NAME);
}
@After
public void tearDown() {
FileUtils.deleteContentsAndDir(mFile);
}
@Test
public void testScheduleWriteHibernationStates_writesDataThatCanBeRead() {
// GIVEN some data to be written
List<String> toWrite = new ArrayList<>(Arrays.asList("A", "B"));
// WHEN the data is written
mHibernationStateDiskStore.scheduleWriteHibernationStates(toWrite);
mMockScheduledExecutorService.executeScheduledTask();
// THEN the read data is equal to what was written
List<String> storedStrings = mHibernationStateDiskStore.readHibernationStates();
for (int i = 0; i < toWrite.size(); i++) {
assertEquals(toWrite.get(i), storedStrings.get(i));
}
}
@Test
public void testScheduleWriteHibernationStates_laterWritesOverwritePrevious() {
// GIVEN store has some data it is scheduled to write
mHibernationStateDiskStore.scheduleWriteHibernationStates(
new ArrayList<>(Arrays.asList("C", "D")));
// WHEN a write is scheduled with new data
List<String> toWrite = new ArrayList<>(Arrays.asList("A", "B"));
mHibernationStateDiskStore.scheduleWriteHibernationStates(toWrite);
mMockScheduledExecutorService.executeScheduledTask();
// THEN the written data is the last scheduled data
List<String> storedStrings = mHibernationStateDiskStore.readHibernationStates();
for (int i = 0; i < toWrite.size(); i++) {
assertEquals(toWrite.get(i), storedStrings.get(i));
}
}
/**
* Mock proto read / writer that just writes and reads a list of String data.
*/
private final class MockProtoReadWriter implements ProtoReadWriter<List<String>> {
private static final long FIELD_ID = 1;
@Override
public void writeToProto(@NonNull ProtoOutputStream stream,
@NonNull List<String> data) {
for (int i = 0, size = data.size(); i < size; i++) {
stream.write(FIELD_ID, data.get(i));
}
}
@Nullable
@Override
public List<String> readFromProto(@NonNull ProtoInputStream stream)
throws IOException {
ArrayList<String> list = new ArrayList<>();
while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
list.add(stream.readString(FIELD_ID));
}
return list;
}
}
/**
* Mock scheduled executor service that has minimum implementation and can synchronously
* execute scheduled tasks.
*/
private final class MockScheduledExecutorService implements ScheduledExecutorService {
Runnable mScheduledRunnable = null;
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
mScheduledRunnable = command;
return Mockito.mock(ScheduledFuture.class);
}
@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,
long period, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
long delay, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public void shutdown() {
throw new UnsupportedOperationException();
}
@Override
public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
@Override
public boolean isShutdown() {
return false;
}
@Override
public boolean isTerminated() {
return false;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public <T> Future<T> submit(Callable<T> task) {
throw new UnsupportedOperationException();
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
throw new UnsupportedOperationException();
}
@Override
public Future<?> submit(Runnable task) {
throw new UnsupportedOperationException();
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout,
TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
throw new UnsupportedOperationException();
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
throw new UnsupportedOperationException();
}
@Override
public void execute(Runnable command) {
throw new UnsupportedOperationException();
}
void executeScheduledTask() {
mScheduledRunnable.run();
}
}
}