Introduce API to get hibernation stats

Introduce an API to get relevant stats about hibernated apps. For now,
this only includes the amount of storage saved from the app being
hibernated. This will be used to in user-facing UI surfaces to show how
much storage hibernation has saved the user.

Bug: 213480570
Test: atest AppHibernationServiceTest
CTS-Coverage-Bug: 216383448
Change-Id: I41877aa2f4959b3f2b13731c8336077555379338
This commit is contained in:
Kevin Han
2022-01-20 13:31:23 -08:00
parent d5b09e891d
commit caa924a1c6
7 changed files with 245 additions and 2 deletions

View File

@@ -2244,12 +2244,21 @@ package android.apphibernation {
public class AppHibernationManager {
method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public java.util.List<java.lang.String> getHibernatingPackagesForUser();
method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public java.util.Map<java.lang.String,android.apphibernation.HibernationStats> getHibernationStatsForUser(@NonNull java.util.Set<java.lang.String>);
method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public java.util.Map<java.lang.String,android.apphibernation.HibernationStats> getHibernationStatsForUser();
method @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public boolean isHibernatingForUser(@NonNull String);
method @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public boolean isHibernatingGlobally(@NonNull String);
method @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public void setHibernatingForUser(@NonNull String, boolean);
method @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public void setHibernatingGlobally(@NonNull String, boolean);
}
public final class HibernationStats implements android.os.Parcelable {
method public int describeContents();
method public long getDiskBytesSaved();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.apphibernation.HibernationStats> CREATOR;
}
}
package android.bluetooth {

View File

@@ -24,7 +24,10 @@ import android.content.Context;
import android.os.RemoteException;
import android.os.ServiceManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This class provides an API surface for system apps to manipulate the app hibernation
@@ -129,4 +132,38 @@ public class AppHibernationManager {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the stats from app hibernation for each package provided.
*
* @param packageNames the set of packages to return stats for
* @hide
*/
@SystemApi
@RequiresPermission(value = android.Manifest.permission.MANAGE_APP_HIBERNATION)
public @NonNull Map<String, HibernationStats> getHibernationStatsForUser(
@NonNull Set<String> packageNames) {
try {
return mIAppHibernationService.getHibernationStatsForUser(
new ArrayList(packageNames), mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the stats from app hibernation for all packages for the user
*
* @hide
*/
@SystemApi
@RequiresPermission(value = android.Manifest.permission.MANAGE_APP_HIBERNATION)
public @NonNull Map<String, HibernationStats> getHibernationStatsForUser() {
try {
return mIAppHibernationService.getHibernationStatsForUser(
null /* packageNames */, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2022, 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 android.apphibernation;
parcelable HibernationStats;

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2022 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 android.apphibernation;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Stats for a hibernating package.
* @hide
*/
@SystemApi
public final class HibernationStats implements Parcelable {
private final long mDiskBytesSaved;
/** @hide */
public HibernationStats(long diskBytesSaved) {
mDiskBytesSaved = diskBytesSaved;
}
private HibernationStats(@NonNull Parcel in) {
mDiskBytesSaved = in.readLong();
}
/**
* Get the disk storage saved from hibernation in bytes.
*/
public long getDiskBytesSaved() {
return mDiskBytesSaved;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeLong(mDiskBytesSaved);
}
public static final @NonNull Creator<HibernationStats> CREATOR =
new Creator<HibernationStats>() {
@Override
public HibernationStats createFromParcel(Parcel in) {
return new HibernationStats(in);
}
@Override
public HibernationStats[] newArray(int size) {
return new HibernationStats[size];
}
};
}

View File

@@ -16,6 +16,8 @@
package android.apphibernation;
import android.apphibernation.HibernationStats;
/**
* Binder interface to communicate with AppHibernationService.
* @hide
@@ -26,4 +28,6 @@ interface IAppHibernationService {
boolean isHibernatingGlobally(String packageName);
void setHibernatingGlobally(String packageName, boolean isHibernating);
List<String> getHibernatingPackagesForUser(int userId);
Map<String, HibernationStats> getHibernationStatsForUser(in List<String> packageNames,
int userId);
}

View File

@@ -38,6 +38,7 @@ import android.app.StatsManager.StatsPullAtomCallback;
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;
@@ -221,7 +222,7 @@ public final class AppHibernationService extends SystemService {
}
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.MANAGE_APP_HIBERNATION,
"Caller does not have MANAGE_APP_HIBERNATION permission.");
"Caller did not have permission while calling " + methodName);
userId = handleIncomingUser(userId, methodName);
synchronized (mLock) {
if (!checkUserStatesExist(userId, methodName)) {
@@ -379,6 +380,46 @@ public final class AppHibernationService extends SystemService {
}
}
/**
* 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";
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)) {
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, String.format(
"No hibernation state associated with package %s user %d. Maybe"
+ "the package was uninstalled? ", pkgName, userId));
continue;
}
HibernationStats stats = new HibernationStats(
mGlobalHibernationStates.get(pkgName).savedByte);
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.
@@ -787,6 +828,13 @@ public final class AppHibernationService extends SystemService {
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 void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
@Nullable FileDescriptor err, @NonNull String[] args,

View File

@@ -23,6 +23,7 @@ import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.returnsArgAt;
import static org.mockito.ArgumentMatchers.any;
@@ -40,6 +41,7 @@ import android.app.IActivityManager;
import android.app.usage.UsageEvents.Event;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
import android.apphibernation.HibernationStats;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -68,6 +70,8 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -126,7 +130,7 @@ public final class AppHibernationServiceTest {
mUsageEventListener = mUsageEventListenerCaptor.getValue();
doReturn(mUserInfos).when(mUserManager).getUsers();
doReturn(true).when(mPackageManagerInternal).canQueryPackage(anyInt(), any());
doAnswer(returnsArgAt(2)).when(mIActivityManager).handleIncomingUser(anyInt(), anyInt(),
anyInt(), anyBoolean(), anyBoolean(), any(), any());
@@ -376,6 +380,58 @@ public final class AppHibernationServiceTest {
assertFalse(mAppHibernationService.isHibernatingGlobally(PACKAGE_NAME_1));
}
@Test
public void testGetHibernationStatsForUser_getsStatsForPackage() {
// GIVEN a package is hibernating globally and for a user
mAppHibernationService.setHibernatingGlobally(PACKAGE_NAME_1, true);
mAppHibernationService.setHibernatingForUser(PACKAGE_NAME_1, USER_ID_1, true);
// WHEN we ask for the hibernation stats for the package
Map<String, HibernationStats> stats =
mAppHibernationService.getHibernationStatsForUser(
Set.of(PACKAGE_NAME_1), USER_ID_1);
// THEN the stats exist for the package
assertTrue(stats.containsKey(PACKAGE_NAME_1));
}
@Test
public void testGetHibernationStatsForUser_noExceptionThrownWhenPackageDoesntExist() {
// WHEN we ask for the hibernation stats for a package that doesn't exist
Map<String, HibernationStats> stats =
mAppHibernationService.getHibernationStatsForUser(
Set.of(PACKAGE_NAME_1), USER_ID_1);
// THEN no exception is thrown and empty stats are returned
assertNotNull(stats);
}
@Test
public void testGetHibernationStatsForUser_returnsAllIfNoPackagesSpecified()
throws RemoteException {
// GIVEN an unlocked user with all packages installed and they're all hibernating
UserInfo userInfo =
addUser(USER_ID_2, new String[]{PACKAGE_NAME_1, PACKAGE_NAME_2, PACKAGE_NAME_3});
doReturn(true).when(mUserManager).isUserUnlockingOrUnlocked(USER_ID_2);
mAppHibernationService.onUserUnlocking(new SystemService.TargetUser(userInfo));
mAppHibernationService.setHibernatingGlobally(PACKAGE_NAME_1, true);
mAppHibernationService.setHibernatingForUser(PACKAGE_NAME_1, USER_ID_2, true);
mAppHibernationService.setHibernatingGlobally(PACKAGE_NAME_2, true);
mAppHibernationService.setHibernatingForUser(PACKAGE_NAME_2, USER_ID_2, true);
mAppHibernationService.setHibernatingGlobally(PACKAGE_NAME_3, true);
mAppHibernationService.setHibernatingForUser(PACKAGE_NAME_3, USER_ID_2, true);
// WHEN we ask for the hibernation stats with no package specified
Map<String, HibernationStats> stats =
mAppHibernationService.getHibernationStatsForUser(
null /* packageNames */, USER_ID_2);
// THEN all the package stats are returned
assertTrue(stats.containsKey(PACKAGE_NAME_1));
assertTrue(stats.containsKey(PACKAGE_NAME_2));
assertTrue(stats.containsKey(PACKAGE_NAME_3));
}
/**
* Mock a usage event occurring.
*