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:
@@ -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
|
||||
|
||||
42
core/proto/android/server/apphibernationservice.proto
Normal file
42
core/proto/android/server/apphibernationservice.proto
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user