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:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
core/java/android/apphibernation/HibernationStats.aidl
Normal file
19
core/java/android/apphibernation/HibernationStats.aidl
Normal 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;
|
||||
70
core/java/android/apphibernation/HibernationStats.java
Normal file
70
core/java/android/apphibernation/HibernationStats.java
Normal 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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user