OSDN Git Service

Let the job scheduler know when the user interacts with an app
authorChristopher Tate <ctate@google.com>
Sat, 6 Jan 2018 01:32:36 +0000 (17:32 -0800)
committerChristopher Tate <ctate@google.com>
Wed, 17 Jan 2018 01:37:04 +0000 (17:37 -0800)
Bug: 70297451
Test: manual
Change-Id: I095d86f17a589d8b6531fb71018cfd4276989ba4

core/java/android/app/usage/UsageStatsManagerInternal.java
services/core/java/com/android/server/job/JobSchedulerInternal.java
services/core/java/com/android/server/job/JobSchedulerService.java
services/core/java/com/android/server/job/JobServiceContext.java
services/usage/java/com/android/server/usage/AppIdleHistory.java
services/usage/java/com/android/server/usage/AppStandbyController.java
services/usage/java/com/android/server/usage/UsageStatsService.java

index 5cd0981..fae9364 100644 (file)
@@ -146,6 +146,14 @@ public abstract class UsageStatsManagerInternal {
          * allowed to do work even if they're idle or in a low bucket.
          */
         public abstract void onParoleStateChanged(boolean isParoleOn);
+
+        /**
+         * Optional callback to inform the listener that the app has transitioned into
+         * an active state due to user interaction.
+         */
+        public void onUserInteractionStarted(String packageName, @UserIdInt int userId) {
+            // No-op by default
+        }
     }
 
     /**  Backup/Restore API */
@@ -206,4 +214,17 @@ public abstract class UsageStatsManagerInternal {
      * indicated here before by a call to {@link #setLastJobRunTime(String, int, long)}.
      */
     public abstract long getTimeSinceLastJobRun(String packageName, @UserIdInt int userId);
+
+    /**
+     * Report a few data points about an app's job state at the current time.
+     *
+     * @param packageName the app whose job state is being described
+     * @param userId which user the app is associated with
+     * @param numDeferredJobs the number of pending jobs that were deferred
+     *   due to bucketing policy
+     * @param timeSinceLastJobRun number of milliseconds since the last time one of
+     *   this app's jobs was executed
+     */
+    public abstract void reportAppJobState(String packageName, @UserIdInt int userId,
+            int numDeferredJobs, long timeSinceLastJobRun);
 }
index c97eeaf..d77f187 100644 (file)
@@ -55,6 +55,14 @@ public interface JobSchedulerInternal {
     void removeBackingUpUid(int uid);
     void clearAllBackingUpUids();
 
+    /**
+     * The user has started interacting with the app.  Take any appropriate action.
+     */
+    void reportAppUsage(String packageName, int userId);
+
+    /**
+     * Report a snapshot of sync-related jobs back to the sync manager
+     */
     JobStorePersistStats getPersistStats();
 
     /**
index 733ed3d..cae0402 100644 (file)
@@ -1029,6 +1029,11 @@ public final class JobSchedulerService extends com.android.server.SystemService
         }
     }
 
+    void reportAppUsage(String packageName, int userId) {
+        // This app just transitioned into interactive use or near equivalent, so we should
+        // take a look at its job state for feedback purposes.
+    }
+
     /**
      * Initializes the system service.
      * <p>
@@ -2075,6 +2080,11 @@ public final class JobSchedulerService extends com.android.server.SystemService
         }
 
         @Override
+        public void reportAppUsage(String packageName, int userId) {
+            JobSchedulerService.this.reportAppUsage(packageName, userId);
+        }
+
+        @Override
         public JobStorePersistStats getPersistStats() {
             synchronized (mLock) {
                 return new JobStorePersistStats(mJobs.getPersistStats());
@@ -2132,6 +2142,40 @@ public final class JobSchedulerService extends com.android.server.SystemService
             }
             mInParole = isParoleOn;
         }
+
+        @Override
+        public void onUserInteractionStarted(String packageName, int userId) {
+            final int uid = mLocalPM.getPackageUid(packageName,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
+            if (uid < 0) {
+                // Quietly ignore; the case is already logged elsewhere
+                return;
+            }
+
+            final long sinceLast = sElapsedRealtimeClock.millis() -
+                    mUsageStats.getTimeSinceLastJobRun(packageName, userId);
+            final DeferredJobCounter counter = new DeferredJobCounter();
+            synchronized (mLock) {
+                mJobs.forEachJobForSourceUid(uid, counter);
+            }
+
+            mUsageStats.reportAppJobState(packageName, userId, counter.numDeferred(), sinceLast);
+        }
+    }
+
+    static class DeferredJobCounter implements JobStatusFunctor {
+        private int mDeferred = 0;
+
+        public int numDeferred() {
+            return mDeferred;
+        }
+
+        @Override
+        public void process(JobStatus job) {
+            if (job.getWhenStandbyDeferred() > 0) {
+                mDeferred++;
+            }
+        }
     }
 
     public static int standbyBucketToBucketIndex(int bucket) {
index 709deeb..c81f50d 100644 (file)
@@ -24,6 +24,7 @@ import android.app.job.IJobService;
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
 import android.app.job.JobWorkItem;
+import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -46,6 +47,7 @@ import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
 import com.android.server.job.controllers.JobStatus;
 
 /**
@@ -238,6 +240,11 @@ public final class JobServiceContext implements ServiceConnection {
                 }
             }
 
+            UsageStatsManagerInternal usageStats =
+                    LocalServices.getService(UsageStatsManagerInternal.class);
+            usageStats.setLastJobRunTime(job.getSourcePackageName(), job.getSourceUserId(),
+                    mExecutionStartTimeElapsed);
+
             // Once we'e begun executing a job, we by definition no longer care whether
             // it was inflated from disk with not-yet-coherent delay/deadline bounds.
             job.clearPersistedUtcTimes();
index 0cbda28..2becdf2 100644 (file)
@@ -36,6 +36,8 @@ import android.util.Xml;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerInternal;
 
 import libcore.io.IoUtils;
 
@@ -202,27 +204,23 @@ public class AppIdleHistory {
      * that's in the future, then the usage event is temporary and keeps the app in the specified
      * bucket at least until the timeout is reached. This can be used to keep the app in an
      * elevated bucket for a while until some important task gets to run.
-     * @param packageName
-     * @param userId
-     * @param bucket the bucket to set the app to
+     * @param appUsageHistory the usage record for the app being updated
+     * @param packageName name of the app being updated, for logging purposes
+     * @param newBucket the bucket to set the app to
      * @param elapsedRealtime mark as used time if non-zero
      * @param timeout set the timeout of the specified bucket, if non-zero
      * @return
      */
-    public int reportUsage(String packageName, int userId, int bucket, long elapsedRealtime,
-            long timeout) {
-        ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
-        AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
-                elapsedRealtime, true);
-
+    public AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName,
+            int newBucket, long elapsedRealtime, long timeout) {
         if (elapsedRealtime != 0) {
             appUsageHistory.lastUsedElapsedTime = mElapsedDuration
                     + (elapsedRealtime - mElapsedSnapshot);
             appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
         }
 
-        if (appUsageHistory.currentBucket > bucket) {
-            appUsageHistory.currentBucket = bucket;
+        if (appUsageHistory.currentBucket > newBucket) {
+            appUsageHistory.currentBucket = newBucket;
             if (DEBUG) {
                 Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory
                         .currentBucket
@@ -235,7 +233,26 @@ public class AppIdleHistory {
         }
         appUsageHistory.bucketingReason = REASON_USAGE;
 
-        return appUsageHistory.currentBucket;
+        return appUsageHistory;
+    }
+
+    /**
+     * Mark the app as used and update the bucket if necessary. If there is a timeout specified
+     * that's in the future, then the usage event is temporary and keeps the app in the specified
+     * bucket at least until the timeout is reached. This can be used to keep the app in an
+     * elevated bucket for a while until some important task gets to run.
+     * @param packageName
+     * @param userId
+     * @param newBucket the bucket to set the app to
+     * @param elapsedRealtime mark as used time if non-zero
+     * @param timeout set the timeout of the specified bucket, if non-zero
+     * @return
+     */
+    public AppUsageHistory reportUsage(String packageName, int userId, int newBucket,
+            long nowElapsed, long timeout) {
+        ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+        AppUsageHistory history = getPackageHistory(userHistory, packageName, nowElapsed, true);
+        return reportUsage(history, packageName, newBucket, nowElapsed, timeout);
     }
 
     private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) {
index ff3d586..1bce958 100644 (file)
@@ -27,7 +27,6 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
-
 import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 
@@ -80,6 +79,7 @@ import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
+import com.android.server.usage.AppIdleHistory.AppUsageHistory;
 
 import java.io.File;
 import java.io.PrintWriter;
@@ -189,6 +189,48 @@ public class AppStandbyController {
     private PackageManager mPackageManager;
     Injector mInjector;
 
+    static final ArrayList<StandbyUpdateRecord> sStandbyUpdatePool = new ArrayList<>(4);
+
+    public static class StandbyUpdateRecord {
+        // Identity of the app whose standby state has changed
+        String packageName;
+        int userId;
+
+        // What the standby bucket the app is now in
+        int bucket;
+
+        // Whether the bucket change is because the user has started interacting with the app
+        boolean isUserInteraction;
+
+        StandbyUpdateRecord(String pkgName, int userId, int bucket, boolean isInteraction) {
+            this.packageName = pkgName;
+            this.userId = userId;
+            this.bucket = bucket;
+            this.isUserInteraction = isInteraction;
+        }
+
+        public static StandbyUpdateRecord obtain(String pkgName, int userId,
+                int bucket, boolean isInteraction) {
+            synchronized (sStandbyUpdatePool) {
+                final int size = sStandbyUpdatePool.size();
+                if (size < 1) {
+                    return new StandbyUpdateRecord(pkgName, userId, bucket, isInteraction);
+                }
+                StandbyUpdateRecord r = sStandbyUpdatePool.remove(size - 1);
+                r.packageName = pkgName;
+                r.userId = userId;
+                r.bucket = bucket;
+                r.isUserInteraction = isInteraction;
+                return r;
+            }
+        }
+
+        public void recycle() {
+            synchronized (sStandbyUpdatePool) {
+                sStandbyUpdatePool.add(this);
+            }
+        }
+    }
 
     AppStandbyController(Context context, Looper looper) {
         this(new Injector(context, looper));
@@ -270,11 +312,11 @@ public class AppStandbyController {
                 }
                 if (!packageName.equals(providerPkgName)) {
                     synchronized (mAppIdleLock) {
-                        int newBucket = mAppIdleHistory.reportUsage(packageName, userId,
+                        AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId,
                                 STANDBY_BUCKET_ACTIVE, elapsedRealtime,
                                 elapsedRealtime + 2 * ONE_HOUR);
                         maybeInformListeners(packageName, userId, elapsedRealtime,
-                                newBucket);
+                                appUsage.currentBucket, false);
                     }
                 }
             } catch (PackageManager.NameNotFoundException e) {
@@ -408,7 +450,7 @@ public class AppStandbyController {
                                 STANDBY_BUCKET_EXEMPTED, REASON_DEFAULT);
                     }
                     maybeInformListeners(packageName, userId, elapsedRealtime,
-                            STANDBY_BUCKET_EXEMPTED);
+                            STANDBY_BUCKET_EXEMPTED, false);
                 } else {
                     synchronized (mAppIdleLock) {
                         AppIdleHistory.AppUsageHistory app =
@@ -437,7 +479,7 @@ public class AppStandbyController {
                                 mAppIdleHistory.setAppStandbyBucket(packageName, userId,
                                         elapsedRealtime, newBucket, REASON_TIMEOUT);
                                 maybeInformListeners(packageName, userId, elapsedRealtime,
-                                        newBucket);
+                                        newBucket, false);
                             }
                         }
                     }
@@ -465,13 +507,16 @@ public class AppStandbyController {
     }
 
     private void maybeInformListeners(String packageName, int userId,
-            long elapsedRealtime, int bucket) {
+            long elapsedRealtime, int bucket, boolean userStartedInteracting) {
         synchronized (mAppIdleLock) {
             // TODO: fold these into one call + lookup for efficiency if needed
             if (mAppIdleHistory.shouldInformListeners(packageName, userId,
                     elapsedRealtime, bucket)) {
+                StandbyUpdateRecord r = StandbyUpdateRecord.obtain(packageName, userId,
+                        bucket, userStartedInteracting);
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS,
-                        userId, bucket, packageName));
+                        StandbyUpdateRecord.obtain(packageName, userId,
+                                bucket, userStartedInteracting)));
             }
         }
     }
@@ -557,19 +602,27 @@ public class AppStandbyController {
                     || event.mEventType == UsageEvents.Event.USER_INTERACTION
                     || event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN)) {
 
-                final int newBucket;
+                final AppUsageHistory appHistory = mAppIdleHistory.getAppUsageHistory(
+                        event.mPackage, userId, elapsedRealtime);
+                final int prevBucket = appHistory.currentBucket;
+                final String prevBucketReason = appHistory.bucketingReason;
                 if (event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN) {
-                    newBucket = mAppIdleHistory.reportUsage(event.mPackage, userId,
+                    mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                             STANDBY_BUCKET_WORKING_SET,
                             elapsedRealtime, elapsedRealtime + 2 * ONE_HOUR);
                 } else {
-                    newBucket = mAppIdleHistory.reportUsage(event.mPackage, userId,
+                    mAppIdleHistory.reportUsage(event.mPackage, userId,
                             STANDBY_BUCKET_ACTIVE,
                             elapsedRealtime, elapsedRealtime + 2 * ONE_HOUR);
                 }
 
+                final boolean userStartedInteracting =
+                        appHistory.currentBucket == STANDBY_BUCKET_ACTIVE &&
+                        prevBucket != appHistory.currentBucket &&
+                        prevBucketReason != REASON_USAGE;
                 maybeInformListeners(event.mPackage, userId, elapsedRealtime,
-                        newBucket);
+                        appHistory.currentBucket, userStartedInteracting);
+
                 if (previouslyIdle) {
                     notifyBatteryStats(event.mPackage, userId, false);
                 }
@@ -602,7 +655,7 @@ public class AppStandbyController {
                 userId, elapsedRealtime);
         // Inform listeners if necessary
         if (previouslyIdle != stillIdle) {
-            maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket);
+            maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket, false);
             if (!stillIdle) {
                 notifyBatteryStats(packageName, userId, idle);
             }
@@ -862,8 +915,7 @@ public class AppStandbyController {
             mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
                     reason);
         }
-        maybeInformListeners(packageName, userId, elapsedRealtime,
-                newBucket);
+        maybeInformListeners(packageName, userId, elapsedRealtime, newBucket, false);
     }
 
     @VisibleForTesting
@@ -949,11 +1001,14 @@ public class AppStandbyController {
         return packageName != null && packageName.equals(activeScorer);
     }
 
-    void informListeners(String packageName, int userId, int bucket) {
+    void informListeners(String packageName, int userId, int bucket, boolean userInteraction) {
         final boolean idle = bucket >= STANDBY_BUCKET_RARE;
         synchronized (mPackageAccessListeners) {
             for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
                 listener.onAppIdleStateChanged(packageName, userId, idle, bucket);
+                if (userInteraction) {
+                    listener.onUserInteractionStarted(packageName, userId);
+                }
             }
         }
     }
@@ -1207,7 +1262,9 @@ public class AppStandbyController {
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_INFORM_LISTENERS:
-                    informListeners((String) msg.obj, msg.arg1, msg.arg2);
+                    StandbyUpdateRecord r = (StandbyUpdateRecord) msg.obj;
+                    informListeners(r.packageName, r.userId, r.bucket, r.isUserInteraction);
+                    r.recycle();
                     break;
 
                 case MSG_FORCE_IDLE_STATE:
index 78cc81f..b350f47 100644 (file)
@@ -1022,6 +1022,12 @@ public class UsageStatsService extends SystemService implements
             return mAppStandby.getTimeSinceLastJobRun(packageName, userId);
         }
 
+        @Override
+        public void reportAppJobState(String packageName, int userId,
+                int numDeferredJobs, long timeSinceLastJobRun) {
+        }
+
+        @Override
         public void onActiveAdminAdded(String packageName, int userId) {
             mAppStandby.addActiveDeviceAdmin(packageName, userId);
         }