Allow multiple standby buckets to be set in one IPC

SystemApi to pass a map of packagename to bucket mapping.
Reduces the number of IPC calls to be made by bucketing
ML app.

Bug: 63527785
Test: atest CtsAppUsageHostTestCases
Change-Id: I4172fd30cc310092a1f182ae26267de9148009b7
diff --git a/api/system-current.txt b/api/system-current.txt
index 33fa246..7e514fd 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -613,7 +613,9 @@
 
   public final class UsageStatsManager {
     method public int getAppStandbyBucket(java.lang.String);
+    method public java.util.Map<java.lang.String, java.lang.Integer> getAppStandbyBuckets();
     method public void setAppStandbyBucket(java.lang.String, int);
+    method public void setAppStandbyBuckets(java.util.Map<java.lang.String, java.lang.Integer>);
     method public void whitelistAppTemporarily(java.lang.String, long, android.os.UserHandle);
     field public static final int STANDBY_BUCKET_EXEMPTED = 5; // 0x5
     field public static final int STANDBY_BUCKET_NEVER = 50; // 0x32
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index 4fbbdf2..f089c127 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -19,6 +19,8 @@
 import android.app.usage.UsageEvents;
 import android.content.pm.ParceledListSlice;
 
+import java.util.Map;
+
 /**
  * System private API for talking with the UsageStatsManagerService.
  *
@@ -38,4 +40,6 @@
             in String[] annotations, String action);
     int getAppStandbyBucket(String packageName, String callingPackage, int userId);
     void setAppStandbyBucket(String packageName, int bucket, int userId);
+    Map getAppStandbyBuckets(String callingPackage, int userId);
+    void setAppStandbyBuckets(in Map appBuckets, int userId);
 }
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index d614b20..1fc45c9 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -324,11 +324,11 @@
      * state of the app based on app usage patterns. Standby buckets determine how much an app will
      * be restricted from running background tasks such as jobs, alarms and certain PendingIntent
      * callbacks.
-     * Restrictions increase progressively from {@link #STANDBY_BUCKET_ACTIVE} to
+     * <p>Restrictions increase progressively from {@link #STANDBY_BUCKET_ACTIVE} to
      * {@link #STANDBY_BUCKET_RARE}, with {@link #STANDBY_BUCKET_ACTIVE} being the least
      * restrictive. The battery level of the device might also affect the restrictions.
      *
-     * @return the current standby bucket of the calling app.
+     * @return the current standby bucket of the calling app. One of STANDBY_BUCKET_* constants.
      */
     public @StandbyBuckets int getAppStandbyBucket() {
         try {
@@ -359,7 +359,13 @@
 
     /**
      * {@hide}
-     * Changes the app standby state to the provided bucket.
+     * Changes an app's standby bucket to the provided value. The caller can only set the standby
+     * bucket for a different app than itself.
+     * @param packageName the package name of the app to set the bucket for. A SecurityException
+     *                    will be thrown if the package name is that of the caller.
+     * @param bucket the standby bucket to set it to, which should be one of STANDBY_BUCKET_*.
+     *               Setting a standby bucket outside of the range of STANDBY_BUCKET_ACTIVE to
+     *               STANDBY_BUCKET_NEVER will result in a SecurityException.
      */
     @SystemApi
     @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE)
@@ -373,6 +379,39 @@
 
     /**
      * {@hide}
+     * Returns the current standby bucket of every app that has a bucket assigned to it.
+     * The caller must hold the permission android.permission.PACKAGE_USAGE_STATS. The key of the
+     * returned Map is the package name and the value is the bucket assigned to the package.
+     * @see #getAppStandbyBucket()
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
+    public Map<String, Integer> getAppStandbyBuckets() {
+        try {
+            return (Map<String, Integer>) mService.getAppStandbyBuckets(
+                    mContext.getOpPackageName(), mContext.getUserId());
+        } catch (RemoteException e) {
+        }
+        return Collections.EMPTY_MAP;
+    }
+
+    /**
+     * {@hide}
+     * Changes the app standby bucket for multiple apps at once. The Map is keyed by the package
+     * name and the value is one of STANDBY_BUCKET_*.
+     * @param appBuckets a map of package name to bucket value.
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE)
+    public void setAppStandbyBuckets(Map<String, Integer> appBuckets) {
+        try {
+            mService.setAppStandbyBuckets(appBuckets, mContext.getUserId());
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * {@hide}
      * Temporarily whitelist the specified app for a short duration. This is to allow an app
      * receiving a high priority message to be able to access the network and acquire wakelocks
      * even if the device is in power-save mode or the app is currently considered inactive.
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 979323f..54938eb 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -69,7 +69,9 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import static android.app.ActivityManager.RESIZE_MODE_SYSTEM;
 import static android.app.ActivityManager.RESIZE_MODE_USER;
@@ -1867,10 +1869,24 @@
         String value = getNextArgRequired();
         int bucket = bucketNameToBucketValue(value);
         if (bucket < 0) return -1;
+        boolean multiple = peekNextArg() != null;
+
 
         IUsageStatsManager usm = IUsageStatsManager.Stub.asInterface(ServiceManager.getService(
                 Context.USAGE_STATS_SERVICE));
-        usm.setAppStandbyBucket(packageName, bucketNameToBucketValue(value), userId);
+        if (!multiple) {
+            usm.setAppStandbyBucket(packageName, bucketNameToBucketValue(value), userId);
+        } else {
+            HashMap<String, Integer> buckets = new HashMap<>();
+            buckets.put(packageName, bucket);
+            while ((packageName = getNextArg()) != null) {
+                value = getNextArgRequired();
+                bucket = bucketNameToBucketValue(value);
+                if (bucket < 0) continue;
+                buckets.put(packageName, bucket);
+            }
+            usm.setAppStandbyBuckets(buckets, userId);
+        }
         return 0;
     }
 
@@ -1886,12 +1902,21 @@
                 return -1;
             }
         }
-        String packageName = getNextArgRequired();
+        String packageName = getNextArg();
 
         IUsageStatsManager usm = IUsageStatsManager.Stub.asInterface(ServiceManager.getService(
                 Context.USAGE_STATS_SERVICE));
-        int bucket = usm.getAppStandbyBucket(packageName, null, userId);
-        pw.println(bucket);
+        if (packageName != null) {
+            int bucket = usm.getAppStandbyBucket(packageName, null, userId);
+            pw.println(bucket);
+        } else {
+            Map<String, Integer> buckets = (Map<String, Integer>) usm.getAppStandbyBuckets(
+                    SHELL_PACKAGE_NAME, userId);
+            for (Map.Entry<String, Integer> entry: buckets.entrySet()) {
+                pw.print(entry.getKey()); pw.print(": ");
+                pw.println(entry.getValue());
+            }
+        }
         return 0;
     }
 
diff --git a/services/usage/java/com/android/server/usage/AppIdleHistory.java b/services/usage/java/com/android/server/usage/AppIdleHistory.java
index 6ac4b36..620922a 100644
--- a/services/usage/java/com/android/server/usage/AppIdleHistory.java
+++ b/services/usage/java/com/android/server/usage/AppIdleHistory.java
@@ -42,6 +42,8 @@
 import java.io.FileReader;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Keeps track of recent active state changes in apps.
@@ -334,6 +336,16 @@
         return appUsageHistory.currentBucket;
     }
 
+    public Map<String, Integer> getAppStandbyBuckets(int userId, long elapsedRealtime) {
+        ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+        int size = userHistory.size();
+        HashMap<String, Integer> buckets = new HashMap<>(size);
+        for (int i = 0; i < size; i++) {
+            buckets.put(userHistory.keyAt(i), userHistory.valueAt(i).currentBucket);
+        }
+        return buckets;
+    }
+
     public String getAppStandbyReason(String packageName, int userId, long elapsedRealtime) {
         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
         AppUsageHistory appUsageHistory =
diff --git a/services/usage/java/com/android/server/usage/AppStandbyController.java b/services/usage/java/com/android/server/usage/AppStandbyController.java
index d8086bb..46efbd0 100644
--- a/services/usage/java/com/android/server/usage/AppStandbyController.java
+++ b/services/usage/java/com/android/server/usage/AppStandbyController.java
@@ -84,6 +84,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Manages the standby state of an app, listening to various events.
@@ -774,13 +775,23 @@
             return STANDBY_BUCKET_ACTIVE;
         }
 
-        return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime);
+        synchronized (mAppIdleLock) {
+            return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime);
+        }
+    }
+
+    public Map<String, Integer> getAppStandbyBuckets(int userId, long elapsedRealtime) {
+        synchronized (mAppIdleLock) {
+            return mAppIdleHistory.getAppStandbyBuckets(userId, elapsedRealtime);
+        }
     }
 
     void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket,
             String reason, long elapsedRealtime) {
-        mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
-                reason);
+        synchronized (mAppIdleLock) {
+            mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
+                    reason);
+        }
         maybeInformListeners(packageName, userId, elapsedRealtime,
                 newBucket);
     }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 0572771..15284d5 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -68,7 +68,10 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A service that collects, aggregates, and persists application usage data.
@@ -741,6 +744,68 @@
         }
 
         @Override
+        public Map getAppStandbyBuckets(String callingPackageName, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            try {
+                userId = ActivityManager.getService().handleIncomingUser(
+                        Binder.getCallingPid(), callingUid, userId, false, false,
+                        "getAppStandbyBucket", null);
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
+            if (!hasPermission(callingPackageName)) {
+                throw new SecurityException(
+                        "Don't have permission to query app standby bucket");
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                return mAppStandby.getAppStandbyBuckets(userId,
+                        SystemClock.elapsedRealtime());
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void setAppStandbyBuckets(Map appBuckets, int userId) {
+            getContext().enforceCallingPermission(Manifest.permission.CHANGE_APP_IDLE_STATE,
+                    "No permission to change app standby state");
+
+            final int callingUid = Binder.getCallingUid();
+            try {
+                userId = ActivityManager.getService().handleIncomingUser(
+                        Binder.getCallingPid(), callingUid, userId, false, true,
+                        "setAppStandbyBucket", null);
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                final long elapsedRealtime = SystemClock.elapsedRealtime();
+                Map<String, Integer> buckets = (Map<String, Integer>) appBuckets;
+                for (Map.Entry<String, Integer> entry: buckets.entrySet()) {
+                    String packageName = entry.getKey();
+                    int bucket = entry.getValue();
+                    if (bucket < UsageStatsManager.STANDBY_BUCKET_ACTIVE
+                            || bucket > UsageStatsManager.STANDBY_BUCKET_NEVER) {
+                        throw new IllegalArgumentException(
+                                "Cannot set the standby bucket to " + bucket);
+                    }
+                    // Caller cannot set their own standby state
+                    if (mPackageManagerInternal.getPackageUid(packageName,
+                            PackageManager.MATCH_ANY_USER, userId) == callingUid) {
+                        throw new IllegalArgumentException("Cannot set your own standby bucket");
+                    }
+                    mAppStandby.setAppStandbyBucket(packageName, userId, bucket,
+                            UsageStatsManager.REASON_PREDICTED + ":" + callingUid,
+                            elapsedRealtime);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
         public void whitelistAppTemporarily(String packageName, long duration, int userId)
                 throws RemoteException {
             StringBuilder reason = new StringBuilder(32);