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. *