From caa924a1c65652cdab312f865980391296f4e7ec Mon Sep 17 00:00:00 2001 From: Kevin Han Date: Thu, 20 Jan 2022 13:31:23 -0800 Subject: [PATCH] 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 --- core/api/system-current.txt | 9 +++ .../apphibernation/AppHibernationManager.java | 37 ++++++++++ .../apphibernation/HibernationStats.aidl | 19 +++++ .../apphibernation/HibernationStats.java | 70 +++++++++++++++++++ .../IAppHibernationService.aidl | 4 ++ .../apphibernation/AppHibernationService.java | 50 ++++++++++++- .../AppHibernationServiceTest.java | 58 ++++++++++++++- 7 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 core/java/android/apphibernation/HibernationStats.aidl create mode 100644 core/java/android/apphibernation/HibernationStats.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index b34517d9fe3c5..ce25e8ac4433f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2244,12 +2244,21 @@ package android.apphibernation { public class AppHibernationManager { method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public java.util.List getHibernatingPackagesForUser(); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public java.util.Map getHibernationStatsForUser(@NonNull java.util.Set); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public java.util.Map 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 CREATOR; + } + } package android.bluetooth { diff --git a/core/java/android/apphibernation/AppHibernationManager.java b/core/java/android/apphibernation/AppHibernationManager.java index a36da8816d609..6e3bbccdbef02 100644 --- a/core/java/android/apphibernation/AppHibernationManager.java +++ b/core/java/android/apphibernation/AppHibernationManager.java @@ -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 getHibernationStatsForUser( + @NonNull Set 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 getHibernationStatsForUser() { + try { + return mIAppHibernationService.getHibernationStatsForUser( + null /* packageNames */, mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/apphibernation/HibernationStats.aidl b/core/java/android/apphibernation/HibernationStats.aidl new file mode 100644 index 0000000000000..a92b903f56dca --- /dev/null +++ b/core/java/android/apphibernation/HibernationStats.aidl @@ -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; \ No newline at end of file diff --git a/core/java/android/apphibernation/HibernationStats.java b/core/java/android/apphibernation/HibernationStats.java new file mode 100644 index 0000000000000..2c4db82186434 --- /dev/null +++ b/core/java/android/apphibernation/HibernationStats.java @@ -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 CREATOR = + new Creator() { + @Override + public HibernationStats createFromParcel(Parcel in) { + return new HibernationStats(in); + } + + @Override + public HibernationStats[] newArray(int size) { + return new HibernationStats[size]; + } + }; +} diff --git a/core/java/android/apphibernation/IAppHibernationService.aidl b/core/java/android/apphibernation/IAppHibernationService.aidl index afdb3fe03dad5..11bb6b505cdf0 100644 --- a/core/java/android/apphibernation/IAppHibernationService.aidl +++ b/core/java/android/apphibernation/IAppHibernationService.aidl @@ -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 getHibernatingPackagesForUser(int userId); + Map getHibernationStatsForUser(in List packageNames, + int userId); } \ No newline at end of file diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java index 9d4d1c1b0ff37..366718c65d84b 100644 --- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java +++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java @@ -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 getHibernationStatsForUser( + @Nullable Set packageNames, int userId) { + Map 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 userPackageStates = mUserStates.get(userId); + Set 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 getHibernationStatsForUser( + @Nullable List packageNames, int userId) { + Set 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, diff --git a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java index e40f5439d0c28..e1aa08d9176a7 100644 --- a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java @@ -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 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 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 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. *