Revert "Revert "Add basic launch time prediction.""

This reverts commit 3785977ca6f5e4fd34c1dd26a53841144e063a3e.

Reason for revert: Fixing

Change-Id: I31425d8086e3f1d1d806b84e9339d0acd3f420d4
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index 968c6e5..0f36d32 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -1,5 +1,6 @@
 package com.android.server.usage;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.usage.AppStandbyInfo;
@@ -23,7 +24,7 @@
         try {
             final Class<?> clazz = Class.forName("com.android.server.usage.AppStandbyController",
                     true, loader);
-            final Constructor<?> ctor =  clazz.getConstructor(Context.class);
+            final Constructor<?> ctor = clazz.getConstructor(Context.class);
             return (AppStandbyInternal) ctor.newInstance(context);
         } catch (NoSuchMethodException | InstantiationException
                 | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
@@ -71,6 +72,16 @@
 
     long getTimeSinceLastJobRun(String packageName, int userId);
 
+    void setEstimatedLaunchTime(String packageName, int userId,
+            @CurrentTimeMillisLong long launchTimeMs);
+
+    /**
+     * Returns the saved estimated launch time for the app. Will return {@code Long#MAX_VALUE} if no
+     * value is saved.
+     */
+    @CurrentTimeMillisLong
+    long getEstimatedLaunchTime(String packageName, int userId);
+
     /**
      * Returns the time (in milliseconds) since the app was last interacted with by the user.
      * This can be larger than the current elapsedRealtime, in case it happened before boot or
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
index 6232dfb..393f368 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -25,8 +25,12 @@
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
 import android.content.Context;
+import android.os.Handler;
 import android.os.Looper;
+import android.os.Message;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.util.ArraySet;
@@ -38,7 +42,9 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
 import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.LocalServices;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.utils.AlarmQueue;
 
@@ -53,6 +59,9 @@
             || Log.isLoggable(TAG, Log.DEBUG);
 
     private final PcConstants mPcConstants;
+    private final PcHandler mHandler;
+
+    private final UsageStatsManagerInternal mUsageStatsManagerInternal;
 
     @GuardedBy("mLock")
     private final SparseArrayMap<String, ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
@@ -72,11 +81,34 @@
     @CurrentTimeMillisLong
     private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
 
+    @SuppressWarnings("FieldCanBeLocal")
+    private final EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener =
+            new EstimatedLaunchTimeChangedListener() {
+                @Override
+                public void onEstimatedLaunchTimeChanged(int userId, @NonNull String packageName,
+                        @CurrentTimeMillisLong long newEstimatedLaunchTime) {
+                    final SomeArgs args = SomeArgs.obtain();
+                    args.arg1 = packageName;
+                    args.argi1 = userId;
+                    args.argl1 = newEstimatedLaunchTime;
+                    mHandler.obtainMessage(MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME, args)
+                            .sendToTarget();
+                }
+            };
+
+    private static final int MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME = 0;
+    private static final int MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME = 1;
+
     public PrefetchController(JobSchedulerService service) {
         super(service);
         mPcConstants = new PcConstants();
+        mHandler = new PcHandler(mContext.getMainLooper());
         mThresholdAlarmListener = new ThresholdAlarmListener(
                 mContext, JobSchedulerBackgroundThread.get().getLooper());
+        mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
+
+        mUsageStatsManagerInternal
+                .registerLaunchTimeChangedListener(mEstimatedLaunchTimeChangedListener);
     }
 
     @Override
@@ -146,11 +178,14 @@
     @CurrentTimeMillisLong
     private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName,
             @CurrentTimeMillisLong long now) {
-        Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
+        final Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
         if (nextEstimatedLaunchTime == null || nextEstimatedLaunchTime < now) {
-            // TODO(194532703): get estimated time from UsageStats
-            nextEstimatedLaunchTime = now + 2 * HOUR_IN_MILLIS;
-            mEstimatedLaunchTimes.add(userId, pkgName, nextEstimatedLaunchTime);
+            // Don't query usage stats here because it may have to read from disk.
+            mHandler.obtainMessage(MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME, userId, 0, pkgName)
+                    .sendToTarget();
+            // Store something in the cache so we don't keep posting retrieval messages.
+            mEstimatedLaunchTimes.add(userId, pkgName, Long.MAX_VALUE);
+            return Long.MAX_VALUE;
         }
         return nextEstimatedLaunchTime;
     }
@@ -170,6 +205,42 @@
         return changed;
     }
 
+    private void processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName,
+            @CurrentTimeMillisLong long newEstimatedLaunchTime) {
+        if (DEBUG) {
+            Slog.d(TAG, "Estimated launch time for " + packageToString(userId, pkgName)
+                    + " changed to " + newEstimatedLaunchTime
+                    + " ("
+                    + TimeUtils.formatDuration(newEstimatedLaunchTime - sSystemClock.millis())
+                    + " from now)");
+        }
+
+        synchronized (mLock) {
+            final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
+            if (jobs == null) {
+                if (DEBUG) {
+                    Slog.i(TAG,
+                            "Not caching launch time since we haven't seen any prefetch"
+                                    + " jobs for " + packageToString(userId, pkgName));
+                }
+            } else {
+                // Don't bother caching the value unless the app has scheduled prefetch jobs
+                // before. This is based on the assumption that if an app has scheduled a
+                // prefetch job before, then it will probably schedule another one again.
+                mEstimatedLaunchTimes.add(userId, pkgName, newEstimatedLaunchTime);
+
+                if (!jobs.isEmpty()) {
+                    final long now = sSystemClock.millis();
+                    final long nowElapsed = sElapsedRealtimeClock.millis();
+                    updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
+                    if (maybeUpdateConstraintForPkgLocked(now, nowElapsed, userId, pkgName)) {
+                        mStateChangedListener.onControllerStateChanged(jobs);
+                    }
+                }
+            }
+        }
+    }
+
     @GuardedBy("mLock")
     private boolean updateConstraintLocked(@NonNull JobStatus jobStatus,
             @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
@@ -289,6 +360,49 @@
         }
     }
 
+    private class PcHandler extends Handler {
+        PcHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME:
+                    final int userId = msg.arg1;
+                    final String pkgName = (String) msg.obj;
+                    // It's okay to get the time without holding the lock since all updates to
+                    // the local cache go through the handler (and therefore will be sequential).
+                    final long nextEstimatedLaunchTime = mUsageStatsManagerInternal
+                            .getEstimatedPackageLaunchTime(pkgName, userId);
+                    if (DEBUG) {
+                        Slog.d(TAG, "Retrieved launch time for "
+                                + packageToString(userId, pkgName)
+                                + " of " + nextEstimatedLaunchTime
+                                + " (" + TimeUtils.formatDuration(
+                                        nextEstimatedLaunchTime - sSystemClock.millis())
+                                + " from now)");
+                    }
+                    synchronized (mLock) {
+                        final Long curEstimatedLaunchTime =
+                                mEstimatedLaunchTimes.get(userId, pkgName);
+                        if (curEstimatedLaunchTime == null
+                                || nextEstimatedLaunchTime != curEstimatedLaunchTime) {
+                            processUpdatedEstimatedLaunchTime(
+                                    userId, pkgName, nextEstimatedLaunchTime);
+                        }
+                    }
+                    break;
+
+                case MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME:
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    processUpdatedEstimatedLaunchTime(args.argi1, (String) args.arg1, args.argl1);
+                    args.recycle();
+                    break;
+            }
+        }
+    }
+
     @VisibleForTesting
     class PcConstants {
         private boolean mShouldReevaluateConstraints = false;
@@ -366,7 +480,8 @@
                 final String pkgName = mEstimatedLaunchTimes.keyAt(u, p);
                 final long estimatedLaunchTime = mEstimatedLaunchTimes.valueAt(u, p);
 
-                pw.print("<" + userId + ">" + pkgName + ": ");
+                pw.print(packageToString(userId, pkgName));
+                pw.print(": ");
                 pw.print(estimatedLaunchTime);
                 pw.print(" (");
                 TimeUtils.formatDuration(estimatedLaunchTime - now, pw,
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index 187422b..8b17512 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -32,6 +32,8 @@
 
 import static com.android.server.usage.AppStandbyController.isUserUsage;
 
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.ElapsedRealtimeLong;
 import android.app.usage.AppStandbyInfo;
 import android.app.usage.UsageStatsManager;
 import android.os.SystemClock;
@@ -115,6 +117,8 @@
     // Reason why the app was last marked for restriction.
     private static final String ATTR_LAST_RESTRICTION_ATTEMPT_REASON =
             "lastRestrictionAttemptReason";
+    // The next estimated launch time of the app, in ms since epoch.
+    private static final String ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME = "nextEstimatedAppLaunchTime";
 
     // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
     private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
@@ -151,6 +155,9 @@
         int lastInformedBucket;
         // The last time a job was run for this app, using elapsed timebase
         long lastJobRunTime;
+        // The estimated time the app will be launched next, in milliseconds since epoch.
+        @CurrentTimeMillisLong
+        long nextEstimatedLaunchTime;
         // When should the bucket active state timeout, in elapsed timebase, if greater than
         // lastUsedElapsedTime.
         // This is used to keep the app in a high bucket regardless of other timeouts and
@@ -411,6 +418,17 @@
     }
 
     /**
+     * Marks the next time the app is expected to be launched, in the current millis timebase.
+     */
+    public void setEstimatedLaunchTime(String packageName, int userId,
+            @ElapsedRealtimeLong long nowElapsed, @CurrentTimeMillisLong long launchTime) {
+        ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+        AppUsageHistory appUsageHistory =
+                getPackageHistory(userHistory, packageName, nowElapsed, true);
+        appUsageHistory.nextEstimatedLaunchTime = launchTime;
+    }
+
+    /**
      * Marks the last time a job was run, with the given elapsedRealtime. The time stored is
      * based on the elapsed timebase.
      * @param packageName
@@ -443,6 +461,23 @@
     }
 
     /**
+     * Returns the next estimated launch time of this app. Will return {@link Long#MAX_VALUE} if
+     * there's no estimated time.
+     */
+    @CurrentTimeMillisLong
+    public long getEstimatedLaunchTime(String packageName, int userId, long nowElapsed) {
+        ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+        AppUsageHistory appUsageHistory =
+                getPackageHistory(userHistory, packageName, nowElapsed, false);
+        // Don't adjust the default, else it'll wrap around to a positive value
+        if (appUsageHistory == null
+                || appUsageHistory.nextEstimatedLaunchTime < System.currentTimeMillis()) {
+            return Long.MAX_VALUE;
+        }
+        return appUsageHistory.nextEstimatedLaunchTime;
+    }
+
+    /**
      * Returns the time since the last job was run for this app. This can be larger than the
      * current elapsedRealtime, in case it happened before boot or a really large value if no jobs
      * were ever run.
@@ -671,6 +706,8 @@
                                 Slog.wtf(TAG, "Unable to read last restrict reason", nfe);
                             }
                         }
+                        appUsageHistory.nextEstimatedLaunchTime = getLongValue(parser,
+                                ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME, 0);
                         appUsageHistory.lastInformedBucket = -1;
                         userHistory.put(packageName, appUsageHistory);
                     }
@@ -753,6 +790,10 @@
                 }
                 xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON,
                         Integer.toHexString(history.lastRestrictReason));
+                if (history.nextEstimatedLaunchTime > 0) {
+                    xml.attribute(null, ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME,
+                            Long.toString(history.nextEstimatedLaunchTime));
+                }
                 xml.endTag(null, TAG_PACKAGE);
             }
 
@@ -779,6 +820,7 @@
         idpw.println(" App Standby States:");
         idpw.increaseIndent();
         ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
+        final long now = System.currentTimeMillis();
         final long elapsedRealtime = SystemClock.elapsedRealtime();
         final long totalElapsedTime = getElapsedTime(elapsedRealtime);
         final long screenOnTime = getScreenOnTime(elapsedRealtime);
@@ -819,6 +861,10 @@
                 idpw.print(" lastRestrictReason="
                         + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason));
             }
+            if (appUsageHistory.nextEstimatedLaunchTime > 0) {
+                idpw.print(" nextEstimatedLaunchTime=");
+                TimeUtils.formatDuration(appUsageHistory.nextEstimatedLaunchTime - now, idpw);
+            }
             idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
             idpw.println();
         }
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 096211b..abbae4e 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -54,6 +54,7 @@
 import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -1087,6 +1088,24 @@
     }
 
     @Override
+    public void setEstimatedLaunchTime(String packageName, int userId,
+            @CurrentTimeMillisLong long launchTime) {
+        final long nowElapsed = mInjector.elapsedRealtime();
+        synchronized (mAppIdleLock) {
+            mAppIdleHistory.setEstimatedLaunchTime(packageName, userId, nowElapsed, launchTime);
+        }
+    }
+
+    @Override
+    @CurrentTimeMillisLong
+    public long getEstimatedLaunchTime(String packageName, int userId) {
+        final long elapsedRealtime = mInjector.elapsedRealtime();
+        synchronized (mAppIdleLock) {
+            return mAppIdleHistory.getEstimatedLaunchTime(packageName, userId, elapsedRealtime);
+        }
+    }
+
+    @Override
     public long getTimeSinceLastUsedByUser(String packageName, int userId) {
         final long elapsedRealtime = mInjector.elapsedRealtime();
         synchronized (mAppIdleLock) {
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index 71fae3d..0f5cd4e 100644
--- a/core/java/android/app/usage/UsageEvents.java
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -15,6 +15,7 @@
  */
 package android.app.usage;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -584,6 +585,7 @@
          * <p/>
          * See {@link System#currentTimeMillis()}.
          */
+        @CurrentTimeMillisLong
         public long getTimeStamp() {
             return mTimeStamp;
         }
@@ -801,6 +803,9 @@
      * @return true if an event was available, false if there are no more events.
      */
     public boolean getNextEvent(Event eventOut) {
+        if (eventOut == null) {
+            throw new IllegalArgumentException("Given eventOut must not be null");
+        }
         if (mIndex >= mEventCount) {
             return false;
         }
diff --git a/services/core/java/android/app/usage/UsageStatsManagerInternal.java b/services/core/java/android/app/usage/UsageStatsManagerInternal.java
index b2226d1..21fc19e 100644
--- a/services/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/services/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -16,6 +16,7 @@
 
 package android.app.usage;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -234,6 +235,10 @@
     public abstract void setLastJobRunTime(String packageName, @UserIdInt int userId,
             long elapsedRealtime);
 
+    /** Returns the estimated time that the app will be launched, in milliseconds since epoch. */
+    @CurrentTimeMillisLong
+    public abstract long getEstimatedPackageLaunchTime(String packageName, @UserIdInt int userId);
+
     /**
      * Returns the time in millis since a job was executed for this app, in elapsed realtime
      * timebase. This value can be larger than the current elapsed realtime if the job was executed
@@ -340,4 +345,21 @@
 
     /** Unregister a listener from being notified of every new usage event. */
     public abstract void unregisterListener(@NonNull UsageEventListener listener);
+
+    /**
+     * Listener interface for estimated launch time changes.
+     */
+    public interface EstimatedLaunchTimeChangedListener {
+        /** Callback to inform listeners when estimated launch times change. */
+        void onEstimatedLaunchTimeChanged(@UserIdInt int userId, @NonNull String packageName,
+                @CurrentTimeMillisLong long newEstimatedLaunchTime);
+    }
+
+    /** Register a listener that will be notified of every estimated launch time change. */
+    public abstract void registerLaunchTimeChangedListener(
+            @NonNull EstimatedLaunchTimeChangedListener listener);
+
+    /** Unregister a listener from being notified of every estimated launch time change. */
+    public abstract void unregisterLaunchTimeChangedListener(
+            @NonNull EstimatedLaunchTimeChangedListener listener);
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
index 98e089e..e5b2d14 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -20,15 +20,33 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import static com.android.server.job.JobSchedulerService.sSystemClock;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
 
+import android.app.AlarmManager;
+import android.app.job.JobInfo;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ServiceInfo;
+import android.os.Looper;
 import android.os.SystemClock;
 import android.provider.DeviceConfig;
 
@@ -42,7 +60,9 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
@@ -55,15 +75,26 @@
 
 @RunWith(AndroidJUnit4.class)
 public class PrefetchControllerTest {
+    private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
+    private static final int SOURCE_USER_ID = 0;
+    private static final int CALLING_UID = 1000;
+    private static final long DEFAULT_WAIT_MS = 3000;
+    private static final String TAG_PREFETCH = "*job.prefetch*";
+
     private PrefetchController mPrefetchController;
     private PcConstants mPcConstants;
     private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
+    private EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener;
 
     private MockitoSession mMockingSession;
     @Mock
+    private AlarmManager mAlarmManager;
+    @Mock
     private Context mContext;
     @Mock
     private JobSchedulerService mJobSchedulerService;
+    @Mock
+    private UsageStatsManagerInternal mUsageStatsManagerInternal;
 
     @Before
     public void setUp() {
@@ -77,6 +108,11 @@
         // Called in StateController constructor.
         when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
         when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
+        // Called in PrefetchController constructor.
+        doReturn(mUsageStatsManagerInternal)
+                .when(() -> LocalServices.getService(UsageStatsManagerInternal.class));
+        when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
+        when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager);
         // Used in PrefetchController.PcConstants
         doAnswer((Answer<Void>) invocationOnMock -> null)
                 .when(() -> DeviceConfig.addOnPropertiesChangedListener(
@@ -93,7 +129,7 @@
         // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions
         // in the past, and PrefetchController sometimes floors values at 0, so if the test time
         // causes sessions with negative timestamps, they will fail.
-        JobSchedulerService.sSystemClock =
+        sSystemClock =
                 getShiftedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC),
                         24 * HOUR_IN_MILLIS);
         JobSchedulerService.sUptimeMillisClock = getShiftedClock(
@@ -105,8 +141,14 @@
 
         // Initialize real objects.
         // Capture the listeners.
+        ArgumentCaptor<EstimatedLaunchTimeChangedListener> eltListenerCaptor =
+                ArgumentCaptor.forClass(EstimatedLaunchTimeChangedListener.class);
         mPrefetchController = new PrefetchController(mJobSchedulerService);
         mPcConstants = mPrefetchController.getPcConstants();
+
+        verify(mUsageStatsManagerInternal)
+                .registerLaunchTimeChangedListener(eltListenerCaptor.capture());
+        mEstimatedLaunchTimeChangedListener = eltListenerCaptor.getValue();
     }
 
     @After
@@ -116,6 +158,29 @@
         }
     }
 
+    private JobStatus createJobStatus(String testTag, int jobId) {
+        JobInfo jobInfo = new JobInfo.Builder(jobId,
+                new ComponentName(mContext, "TestPrefetchJobService"))
+                .setPrefetch(true)
+                .build();
+        return createJobStatus(testTag, SOURCE_PACKAGE, CALLING_UID, jobInfo);
+    }
+
+    private static JobStatus createJobStatus(String testTag, String packageName, int callingUid,
+            JobInfo jobInfo) {
+        JobStatus js = JobStatus.createFromJobInfo(
+                jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
+        js.serviceInfo = mock(ServiceInfo.class);
+        // Make sure Doze and background-not-restricted don't affect tests.
+        js.setDeviceNotDozingConstraintSatisfied(/* nowElapsed */ sElapsedRealtimeClock.millis(),
+                /* state */ true, /* allowlisted */false);
+        js.setBackgroundNotRestrictedConstraintSatisfied(
+                sElapsedRealtimeClock.millis(), true, false);
+        js.setTareWealthConstraintSatisfied(sElapsedRealtimeClock.millis(), true);
+        js.setExpeditedJobTareApproved(sElapsedRealtimeClock.millis(), true);
+        return js;
+    }
+
     private Clock getShiftedClock(Clock clock, long incrementMs) {
         return Clock.offset(clock, Duration.ofMillis(incrementMs));
     }
@@ -125,6 +190,15 @@
         synchronized (mPrefetchController.mLock) {
             mPrefetchController.prepareForUpdatedConstantsLocked();
             mPcConstants.processConstantLocked(mDeviceConfigPropertiesBuilder.build(), key);
+            mPrefetchController.onConstantsUpdatedLocked();
+        }
+    }
+
+    private void trackJobs(JobStatus... jobs) {
+        for (JobStatus job : jobs) {
+            synchronized (mPrefetchController.mLock) {
+                mPrefetchController.maybeStartTrackingJobLocked(job, null);
+            }
         }
     }
 
@@ -147,4 +221,100 @@
 
         assertEquals(24 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
     }
+
+    @Test
+    public void testConstantsUpdating_ThresholdChangesAlarms() {
+        final long launchDelayMs = 11 * HOUR_IN_MILLIS;
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + launchDelayMs);
+        JobStatus jobStatus = createJobStatus("testConstantsUpdating_ThresholdChangesAlarms", 1);
+        trackJobs(jobStatus);
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        inOrder.verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
+                .setWindow(
+                        anyInt(), eq(sElapsedRealtimeClock.millis() + 4 * HOUR_IN_MILLIS),
+                        anyLong(), eq(TAG_PREFETCH), any(), any());
+
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 3 * HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
+                .setWindow(
+                        anyInt(), eq(sElapsedRealtimeClock.millis() + 8 * HOUR_IN_MILLIS),
+                        anyLong(), eq(TAG_PREFETCH), any(), any());
+    }
+
+    @Test
+    public void testConstraintNotSatisfiedWhenLaunchLate() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+
+        final JobStatus job = createJobStatus("testConstraintNotSatisfiedWhenLaunchLate", 1);
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + 10 * HOUR_IN_MILLIS);
+        trackJobs(job);
+        verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        assertFalse(job.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+    }
+
+    @Test
+    public void testConstraintSatisfiedWhenLaunchSoon() {
+        final JobStatus job = createJobStatus("testConstraintSatisfiedWhenLaunchSoon", 2);
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + MINUTE_IN_MILLIS);
+        trackJobs(job);
+        verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        assertTrue(job.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+    }
+
+    @Test
+    public void testEstimatedLaunchTimeChangedToLate() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + HOUR_IN_MILLIS);
+
+        InOrder inOrder = inOrder(mUsageStatsManagerInternal);
+
+        JobStatus jobStatus = createJobStatus("testEstimatedLaunchTimeChangedToLate", 1);
+        trackJobs(jobStatus);
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+
+        mEstimatedLaunchTimeChangedListener.onEstimatedLaunchTimeChanged(SOURCE_USER_ID,
+                SOURCE_PACKAGE, sSystemClock.millis() + 10 * HOUR_IN_MILLIS);
+
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS).times(0))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+    }
+
+    @Test
+    public void testEstimatedLaunchTimeChangedToSoon() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + 10 * HOUR_IN_MILLIS);
+
+        InOrder inOrder = inOrder(mUsageStatsManagerInternal);
+
+        JobStatus jobStatus = createJobStatus("testEstimatedLaunchTimeChangedToSoon", 1);
+        trackJobs(jobStatus);
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+
+        mEstimatedLaunchTimeChangedListener.onEstimatedLaunchTimeChanged(SOURCE_USER_ID,
+                SOURCE_PACKAGE, sSystemClock.millis() + MINUTE_IN_MILLIS);
+
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS).times(0))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/usage/UserUsageStatsServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/usage/UserUsageStatsServiceTest.java
index 24c58f4..1542b01 100644
--- a/services/tests/mockingservicestests/src/com/android/server/usage/UserUsageStatsServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/usage/UserUsageStatsServiceTest.java
@@ -18,8 +18,11 @@
 
 import static android.app.usage.UsageEvents.Event.ACTIVITY_RESUMED;
 import static android.app.usage.UsageEvents.Event.APP_COMPONENT_USED;
+import static android.app.usage.UsageEvents.Event.NOTIFICATION_SEEN;
 
+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.Mockito.mockitoSession;
 
@@ -28,6 +31,7 @@
 import android.content.Context;
 import android.os.SystemClock;
 import android.text.format.DateUtils;
+import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -47,6 +51,8 @@
 
 @RunWith(AndroidJUnit4.class)
 public class UserUsageStatsServiceTest {
+    private static final String TAG = UserUsageStatsServiceTest.class.getSimpleName();
+
     private static final int TEST_USER_ID = 0;
     private static final String TEST_PACKAGE_NAME = "test.package";
     private static final long TIME_INTERVAL_MILLIS = DateUtils.DAY_IN_MILLIS;
@@ -54,6 +60,8 @@
     private UserUsageStatsService mService;
     private MockitoSession mMockitoSession;
 
+    private File mDir;
+
     @Mock
     private Context mContext;
     @Mock
@@ -66,8 +74,11 @@
                 .strictness(Strictness.LENIENT)
                 .startMocking();
 
-        File dir = new File(InstrumentationRegistry.getContext().getCacheDir(), "test");
-        mService = new UserUsageStatsService(mContext, TEST_USER_ID, dir, mStatsUpdatedListener);
+        // Deleting in tearDown() doesn't always work, so adding a unique suffix to each test
+        // directory to ensure sequential test runs don't interfere with each other.
+        mDir = new File(InstrumentationRegistry.getContext().getCacheDir(),
+                "test_" + System.currentTimeMillis());
+        mService = new UserUsageStatsService(mContext, TEST_USER_ID, mDir, mStatsUpdatedListener);
 
         HashMap<String, Long> installedPkgs = new HashMap<>();
         installedPkgs.put(TEST_PACKAGE_NAME, System.currentTimeMillis());
@@ -77,6 +88,9 @@
 
     @After
     public void tearDown() {
+        if (mDir != null && mDir.exists() && !mDir.delete()) {
+            Log.d(TAG, "Failed to delete test directory");
+        }
         if (mMockitoSession != null) {
             mMockitoSession.finishMocking();
         }
@@ -88,6 +102,9 @@
         event.mPackage = TEST_PACKAGE_NAME;
         mService.reportEvent(event);
 
+        // Force persist the event instead of waiting for it to be processed on the handler.
+        mService.persistActiveStats();
+
         long now = System.currentTimeMillis();
         long startTime = now - TIME_INTERVAL_MILLIS;
         UsageEvents events = mService.queryEventsForPackage(
@@ -112,6 +129,9 @@
         event.mPackage = TEST_PACKAGE_NAME;
         mService.reportEvent(event);
 
+        // Force persist the event instead of waiting for it to be processed on the handler.
+        mService.persistActiveStats();
+
         long now = System.currentTimeMillis();
         long startTime = now - TIME_INTERVAL_MILLIS;
         UsageEvents events = mService.queryEventsForPackage(
@@ -127,4 +147,36 @@
         }
         assertFalse(hasTestEvent);
     }
+
+    @Test
+    public void testQueryEarliestEventsForPackage() {
+        Event event1 = new Event(NOTIFICATION_SEEN, SystemClock.elapsedRealtime());
+        event1.mPackage = TEST_PACKAGE_NAME;
+        mService.reportEvent(event1);
+        Event event2 = new Event(ACTIVITY_RESUMED, SystemClock.elapsedRealtime());
+        event2.mPackage = TEST_PACKAGE_NAME;
+        mService.reportEvent(event2);
+
+        // Force persist the events instead of waiting for them to be processed on the handler.
+        mService.persistActiveStats();
+
+        long now = System.currentTimeMillis();
+        long startTime = now - TIME_INTERVAL_MILLIS;
+        UsageEvents events = mService.queryEarliestEventsForPackage(
+                startTime, now, TEST_PACKAGE_NAME, ACTIVITY_RESUMED);
+
+        assertNotNull(events);
+        boolean hasTestEvent = false;
+        int count = 0;
+        while (events.hasNextEvent()) {
+            count++;
+            Event outEvent = new Event();
+            events.getNextEvent(outEvent);
+            if (outEvent.mEventType == ACTIVITY_RESUMED) {
+                hasTestEvent = true;
+            }
+        }
+        assertTrue(hasTestEvent);
+        assertEquals(2, count);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
index b2dacab..75bd2cc 100644
--- a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
+++ b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
@@ -67,9 +67,10 @@
     private static final UsageStatsDatabase.StatCombiner<IntervalStats> mIntervalStatsVerifier =
             new UsageStatsDatabase.StatCombiner<IntervalStats>() {
                 @Override
-                public void combine(IntervalStats stats, boolean mutable,
+                public boolean combine(IntervalStats stats, boolean mutable,
                         List<IntervalStats> accResult) {
                     accResult.add(stats);
+                    return true;
                 }
             };
 
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index ad042dd..cc33f88 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -799,8 +799,10 @@
          * @param stats             The {@link IntervalStats} object selected.
          * @param mutable           Whether or not the data inside the stats object is mutable.
          * @param accumulatedResult The list to which to add extracted data.
+         * @return Whether or not to continue providing new stats to this combiner. If {@code false}
+         * is returned, then combine will no longer be called.
          */
-        void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
+        boolean combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
     }
 
     /**
@@ -863,8 +865,9 @@
 
                 try {
                     readLocked(f, stats);
-                    if (beginTime < stats.endTime) {
-                        combiner.combine(stats, false, results);
+                    if (beginTime < stats.endTime
+                            && !combiner.combine(stats, false, results)) {
+                        break;
                     }
                 } catch (Exception e) {
                     Slog.e(TAG, "Failed to read usage stats file", e);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index f0ceff1..6dbf4c5 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -29,8 +29,10 @@
 import static android.app.usage.UsageEvents.Event.USER_UNLOCKED;
 import static android.app.usage.UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY;
 import static android.app.usage.UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 
 import android.Manifest;
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -84,6 +86,7 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.os.BackgroundThread;
@@ -94,6 +97,7 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
+import com.android.server.utils.AlarmQueue;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
@@ -134,8 +138,15 @@
 
     private static final long TEN_SECONDS = 10 * 1000;
     private static final long TWENTY_MINUTES = 20 * 60 * 1000;
+    private static final long ONE_DAY = 24 * HOUR_IN_MILLIS;
+    private static final long ONE_WEEK = 7 * ONE_DAY;
     private static final long FLUSH_INTERVAL = COMPRESS_TIME ? TEN_SECONDS : TWENTY_MINUTES;
     static final long TIME_CHANGE_THRESHOLD_MILLIS = 2 * 1000; // Two seconds.
+    /**
+     * Used when we can't determine the next app launch time. Assume the app will get launched
+     * this amount of time in the future.
+     */
+    private static final long UNKNOWN_LAUNCH_TIME_DELAY_MS = 365 * ONE_DAY;
 
     private static final boolean ENABLE_KERNEL_UPDATES = true;
     private static final File KERNEL_COUNTER_FILE = new File("/proc/uid_procstat/set");
@@ -160,6 +171,9 @@
     static final int MSG_UNLOCKED_USER = 5;
     static final int MSG_PACKAGE_REMOVED = 6;
     static final int MSG_ON_START = 7;
+    static final int MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK = 8;
+    static final int MSG_NOTIFY_ESTIMATED_LAUNCH_TIME_CHANGED = 9;
+    static final int MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED = 10;
 
     private final Object mLock = new Object();
     Handler mHandler;
@@ -194,8 +208,12 @@
     private final SparseArray<LinkedList<Event>> mReportedEvents = new SparseArray<>();
     final SparseArray<ArraySet<String>> mUsageReporters = new SparseArray();
     final SparseArray<ActivityData> mVisibleActivities = new SparseArray();
+    @GuardedBy("mLock")
+    private final SparseArray<LaunchTimeAlarmQueue> mLaunchTimeAlarmQueues = new SparseArray<>();
     private final ArraySet<UsageStatsManagerInternal.UsageEventListener> mUsageEventListeners =
             new ArraySet<>();
+    private final CopyOnWriteArraySet<UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener>
+            mEstimatedLaunchTimeChangedListeners = new CopyOnWriteArraySet<>();
 
     private static class ActivityData {
         private final String mTaskRootPackage;
@@ -369,6 +387,11 @@
             }
             mUserUnlockedStates.remove(userId);
             mUserState.put(userId, null); // release the service (mainly for GC)
+            LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
+            if (alarmQueue != null) {
+                alarmQueue.removeAllAlarms();
+                mLaunchTimeAlarmQueues.remove(userId);
+            }
         }
     }
 
@@ -415,6 +438,8 @@
             }
             reportEvent(unlockEvent, userId);
 
+            mHandler.obtainMessage(MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK, userId, 0).sendToTarget();
+
             // Remove all the stats stored in memory and in system DE.
             mReportedEvents.remove(userId);
             deleteRecursively(new File(Environment.getDataSystemDeDirectory(userId), "usagestats"));
@@ -437,6 +462,7 @@
      * <br/>
      * Note: DO NOT call this while holding the usage stats lock ({@code mLock}).
      */
+    @Nullable
     private HashMap<String, Long> getInstalledPackages(int userId) {
         if (mPackageManager == null) {
             return null;
@@ -472,6 +498,33 @@
         }
     }
 
+    private class LaunchTimeAlarmQueue extends AlarmQueue<String> {
+        private final int mUserId;
+
+        LaunchTimeAlarmQueue(int userId, @NonNull Context context, @NonNull Looper looper) {
+            super(context, looper, "*usage.launchTime*", "Estimated launch times", true, 30_000L);
+            mUserId = userId;
+        }
+
+        @Override
+        protected boolean isForUser(@NonNull String key, int userId) {
+            return mUserId == userId;
+        }
+
+        @Override
+        protected void processExpiredAlarms(@NonNull ArraySet<String> expired) {
+            if (DEBUG) {
+                Slog.d(TAG, "Processing " + expired.size() + " expired alarms: "
+                        + expired.toString());
+            }
+            if (expired.size() > 0) {
+                mHandler.obtainMessage(
+                        MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED, mUserId, 0, expired)
+                        .sendToTarget();
+            }
+        }
+    }
+
     private class UserActionsReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -522,6 +575,8 @@
 
     @Override
     public void onStatsReloaded() {
+        // This method ends up being called with the lock held, so we need to be careful how we
+        // call into other things.
         mAppStandby.postOneTimeCheckIdleStates();
     }
 
@@ -556,7 +611,7 @@
     /**
      * Obfuscate both {@link UsageEvents.Event#NOTIFICATION_SEEN} and
      * {@link UsageEvents.Event#NOTIFICATION_INTERRUPTION} events if the provided calling uid does
-     * not hold the {@link android.Manifest.permission.MANAGE_NOTIFICATIONS} permission.
+     * not hold the {@link android.Manifest.permission#MANAGE_NOTIFICATIONS} permission.
      */
     private boolean shouldObfuscateNotificationEvents(int callingPid, int callingUid) {
         if (callingUid == Process.SYSTEM_UID) {
@@ -953,6 +1008,23 @@
                             event.mTaskRootClass, usageSourcePackage);
                     resumedData.lastEvent = Event.ACTIVITY_RESUMED;
                     mVisibleActivities.put(event.mInstanceId, resumedData);
+                    final long estimatedLaunchTime =
+                            mAppStandby.getEstimatedLaunchTime(event.mPackage, userId);
+                    final long now = System.currentTimeMillis();
+                    if (estimatedLaunchTime < now || estimatedLaunchTime > now + ONE_WEEK) {
+                        // If the estimated launch time is in the past or more than a week into
+                        // the future, then we re-estimate a future launch time of less than a week
+                        // from now, so notify listeners of an estimated launch time change.
+                        // Clear the cached value.
+                        if (DEBUG) {
+                            Slog.d(TAG, event.getPackageName()
+                                    + " app launch resetting future launch estimate");
+                        }
+                        mAppStandby.setEstimatedLaunchTime(event.mPackage, userId, 0);
+                        mHandler.obtainMessage(
+                                MSG_NOTIFY_ESTIMATED_LAUNCH_TIME_CHANGED, userId, 0, event.mPackage)
+                                .sendToTarget();
+                    }
                     break;
                 case Event.ACTIVITY_PAUSED:
                     ActivityData pausedData = mVisibleActivities.get(event.mInstanceId);
@@ -1110,6 +1182,11 @@
             Slog.i(TAG, "Removing user " + userId + " and all data.");
             mUserState.remove(userId);
             mAppTimeLimit.onUserRemoved(userId);
+            final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
+            if (alarmQueue != null) {
+                alarmQueue.removeAllAlarms();
+                mLaunchTimeAlarmQueues.remove(userId);
+            }
         }
         mAppStandby.onUserRemoved(userId);
         // Cancel any scheduled jobs for this user since the user is being removed.
@@ -1129,6 +1206,10 @@
                 // when the user service is initialized and package manager is queried.
                 return;
             }
+            final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
+            if (alarmQueue != null) {
+                alarmQueue.removeAlarmForKey(packageName);
+            }
             final UserUsageStatsService userService = mUserState.get(userId);
             if (userService == null) {
                 return;
@@ -1274,6 +1355,7 @@
     /**
      * Called by the Binder stub.
      */
+    @Nullable
     UsageEvents queryEventsForPackage(int userId, long beginTime, long endTime,
             String packageName, boolean includeTaskRoot) {
         synchronized (mLock) {
@@ -1290,6 +1372,183 @@
         }
     }
 
+    @Nullable
+    private UsageEvents queryEarliestAppEvents(int userId, long beginTime, long endTime,
+            int eventType) {
+        synchronized (mLock) {
+            if (!mUserUnlockedStates.contains(userId)) {
+                Slog.w(TAG, "Failed to query earliest events for locked user " + userId);
+                return null;
+            }
+
+            final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId);
+            if (service == null) {
+                return null; // user was stopped or removed
+            }
+            return service.queryEarliestAppEvents(beginTime, endTime, eventType);
+        }
+    }
+
+    @Nullable
+    private UsageEvents queryEarliestEventsForPackage(int userId, long beginTime, long endTime,
+            @NonNull String packageName, int eventType) {
+        synchronized (mLock) {
+            if (!mUserUnlockedStates.contains(userId)) {
+                Slog.w(TAG, "Failed to query earliset package events for locked user " + userId);
+                return null;
+            }
+
+            final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId);
+            if (service == null) {
+                return null; // user was stopped or removed
+            }
+            return service.queryEarliestEventsForPackage(
+                    beginTime, endTime, packageName, eventType);
+        }
+    }
+
+    @CurrentTimeMillisLong
+    long getEstimatedPackageLaunchTime(int userId, String packageName) {
+        long estimatedLaunchTime = mAppStandby.getEstimatedLaunchTime(packageName, userId);
+        final long now = System.currentTimeMillis();
+        if (estimatedLaunchTime < now || estimatedLaunchTime == Long.MAX_VALUE) {
+            estimatedLaunchTime = calculateEstimatedPackageLaunchTime(userId, packageName);
+            mAppStandby.setEstimatedLaunchTime(packageName, userId, estimatedLaunchTime);
+
+            synchronized (mLock) {
+                LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
+                if (alarmQueue == null) {
+                    alarmQueue = new LaunchTimeAlarmQueue(
+                            userId, getContext(), BackgroundThread.get().getLooper());
+                    mLaunchTimeAlarmQueues.put(userId, alarmQueue);
+                }
+                alarmQueue.addAlarm(packageName,
+                        SystemClock.elapsedRealtime() + (estimatedLaunchTime - now));
+            }
+        }
+        return estimatedLaunchTime;
+    }
+
+    @CurrentTimeMillisLong
+    private long calculateEstimatedPackageLaunchTime(int userId, String packageName) {
+        synchronized (mLock) {
+            final long endTime = System.currentTimeMillis();
+            final long beginTime = endTime - ONE_WEEK;
+            final long unknownTime = endTime + UNKNOWN_LAUNCH_TIME_DELAY_MS;
+            final UsageEvents events = queryEarliestEventsForPackage(
+                    userId, beginTime, endTime, packageName, Event.ACTIVITY_RESUMED);
+            if (events == null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "No events for " + userId + ":" + packageName);
+                }
+                return unknownTime;
+            }
+            final UsageEvents.Event event = new UsageEvents.Event();
+            final boolean hasMoreThan24HoursOfHistory;
+            if (events.getNextEvent(event)) {
+                hasMoreThan24HoursOfHistory = endTime - event.getTimeStamp() > ONE_DAY;
+                if (DEBUG) {
+                    Slog.d(TAG, userId + ":" + packageName + " history > 24 hours="
+                            + hasMoreThan24HoursOfHistory);
+                }
+            } else {
+                if (DEBUG) {
+                    Slog.d(TAG, userId + ":" + packageName + " has no events");
+                }
+                return unknownTime;
+            }
+            do {
+                if (event.getEventType() == Event.ACTIVITY_RESUMED) {
+                    final long timestamp = event.getTimeStamp();
+                    final long nextLaunch =
+                            calculateNextLaunchTime(hasMoreThan24HoursOfHistory, timestamp);
+                    if (nextLaunch > endTime) {
+                        return nextLaunch;
+                    }
+                }
+            } while (events.getNextEvent(event));
+            return unknownTime;
+        }
+    }
+
+    @CurrentTimeMillisLong
+    private static long calculateNextLaunchTime(
+            boolean hasMoreThan24HoursOfHistory, long eventTimestamp) {
+        // For our estimates, we assume the user opens an app at consistent times
+        // (ie. like clockwork).
+        // If the app has more than 24 hours of history, then we assume the user will
+        // reopen the app at the same time on a specific day.
+        // If the app has less than 24 hours of history (meaning it was likely just
+        // installed), then we assume the user will open it at exactly the same time
+        // on the following day.
+        if (hasMoreThan24HoursOfHistory) {
+            return eventTimestamp + ONE_WEEK;
+        } else {
+            return eventTimestamp + ONE_DAY;
+        }
+    }
+
+    private void handleEstimatedLaunchTimesOnUserUnlock(int userId) {
+        synchronized (mLock) {
+            final long nowElapsed = SystemClock.elapsedRealtime();
+            final long now = System.currentTimeMillis();
+            final long beginTime = now - ONE_WEEK;
+            final UsageEvents events = queryEarliestAppEvents(
+                    userId, beginTime, now, Event.ACTIVITY_RESUMED);
+            if (events == null) {
+                return;
+            }
+            final ArrayMap<String, Boolean> hasMoreThan24HoursOfHistory = new ArrayMap<>();
+            final UsageEvents.Event event = new UsageEvents.Event();
+            LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId);
+            if (alarmQueue == null) {
+                alarmQueue = new LaunchTimeAlarmQueue(
+                        userId, getContext(), BackgroundThread.get().getLooper());
+                mLaunchTimeAlarmQueues.put(userId, alarmQueue);
+            }
+            final ArraySet<String> changedTimes = new ArraySet<>();
+            for (boolean unprocessedEvent = events.getNextEvent(event); unprocessedEvent;
+                    unprocessedEvent = events.getNextEvent(event)) {
+                final String packageName = event.getPackageName();
+                if (!hasMoreThan24HoursOfHistory.containsKey(packageName)) {
+                    boolean hasHistory = now - event.getTimeStamp() > ONE_DAY;
+                    if (DEBUG) {
+                        Slog.d(TAG,
+                                userId + ":" + packageName + " history > 24 hours=" + hasHistory);
+                    }
+                    hasMoreThan24HoursOfHistory.put(packageName, hasHistory);
+                }
+                if (event.getEventType() == Event.ACTIVITY_RESUMED) {
+                    long estimatedLaunchTime =
+                            mAppStandby.getEstimatedLaunchTime(packageName, userId);
+                    if (estimatedLaunchTime < now || estimatedLaunchTime == Long.MAX_VALUE) {
+                        //noinspection ConstantConditions
+                        estimatedLaunchTime = calculateNextLaunchTime(
+                                hasMoreThan24HoursOfHistory.get(packageName), event.getTimeStamp());
+                        mAppStandby.setEstimatedLaunchTime(
+                                packageName, userId, estimatedLaunchTime);
+                    }
+                    if (estimatedLaunchTime < now + ONE_WEEK) {
+                        // Before a user is unlocked, we don't know when the app will be launched,
+                        // so we give callers the UNKNOWN time. Now that we have a better estimate,
+                        // we should notify them of the change.
+                        if (DEBUG) {
+                            Slog.d(TAG, "User " + userId + " unlock resulting in"
+                                    + " estimated launch time change for " + packageName);
+                        }
+                        changedTimes.add(packageName);
+                    }
+                    alarmQueue.addAlarm(packageName, nowElapsed + (estimatedLaunchTime - now));
+                }
+            }
+            if (changedTimes.size() > 0) {
+                mHandler.obtainMessage(
+                        MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED, userId, 0, changedTimes)
+                        .sendToTarget();
+            }
+        }
+    }
+
     /**
      * Called via the local interface.
      */
@@ -1309,6 +1568,22 @@
         }
     }
 
+    /**
+     * Called via the local interface.
+     */
+    private void registerLaunchTimeChangedListener(
+            @NonNull UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener listener) {
+        mEstimatedLaunchTimeChangedListeners.add(listener);
+    }
+
+    /**
+     * Called via the local interface.
+     */
+    private void unregisterLaunchTimeChangedListener(
+            @NonNull UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener listener) {
+        mEstimatedLaunchTimeChangedListeners.remove(listener);
+    }
+
     private String buildFullToken(String packageName, String token) {
         final StringBuilder sb = new StringBuilder(packageName.length() + token.length() + 1);
         sb.append(packageName);
@@ -1564,6 +1839,44 @@
                         loadGlobalComponentUsageLocked();
                     }
                     break;
+                case MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK: {
+                    final int userId = msg.arg1;
+                    handleEstimatedLaunchTimesOnUserUnlock(userId);
+                }
+                break;
+                case MSG_NOTIFY_ESTIMATED_LAUNCH_TIME_CHANGED: {
+                    final int userId = msg.arg1;
+                    final String pkgName = (String) msg.obj;
+                    final long nextEstimatedLaunchTime =
+                            getEstimatedPackageLaunchTime(userId, pkgName);
+                    if (DEBUG) {
+                        Slog.d(TAG, "Notifying listener for " + userId + ":" + pkgName);
+                    }
+                    for (UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener listener :
+                            mEstimatedLaunchTimeChangedListeners) {
+                        listener.onEstimatedLaunchTimeChanged(
+                                userId, pkgName, nextEstimatedLaunchTime);
+                    }
+                }
+                break;
+                case MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED: {
+                    final int userId = msg.arg1;
+                    final ArraySet<String> pkgNames = (ArraySet<String>) msg.obj;
+                    if (DEBUG) {
+                        Slog.d(TAG, "Notifying listeners for " + userId + "-->" + pkgNames);
+                    }
+                    for (int p = pkgNames.size() - 1; p >= 0; --p) {
+                        final String pkgName = pkgNames.valueAt(p);
+                        final long nextEstimatedLaunchTime =
+                                getEstimatedPackageLaunchTime(userId, pkgName);
+                        for (UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener listener :
+                                mEstimatedLaunchTimeChangedListeners) {
+                            listener.onEstimatedLaunchTimeChanged(
+                                    userId, pkgName, nextEstimatedLaunchTime);
+                        }
+                    }
+                }
+                break;
                 default:
                     super.handleMessage(msg);
                     break;
@@ -2463,6 +2776,11 @@
         }
 
         @Override
+        public long getEstimatedPackageLaunchTime(String packageName, int userId) {
+            return UsageStatsService.this.getEstimatedPackageLaunchTime(userId, packageName);
+        }
+
+        @Override
         public long getTimeSinceLastJobRun(String packageName, int userId) {
             return mAppStandby.getTimeSinceLastJobRun(packageName, userId);
         }
@@ -2527,6 +2845,18 @@
         public void unregisterListener(@NonNull UsageEventListener listener) {
             UsageStatsService.this.unregisterListener(listener);
         }
+
+        @Override
+        public void registerLaunchTimeChangedListener(
+                @NonNull EstimatedLaunchTimeChangedListener listener) {
+            UsageStatsService.this.registerLaunchTimeChangedListener(listener);
+        }
+
+        @Override
+        public void unregisterLaunchTimeChangedListener(
+                @NonNull EstimatedLaunchTimeChangedListener listener) {
+            UsageStatsService.this.unregisterLaunchTimeChangedListener(listener);
+        }
     }
 
     private class MyPackageMonitor extends PackageMonitor {
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index c4a8e81..23694fc 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -29,6 +29,8 @@
 import static android.app.usage.UsageStatsManager.INTERVAL_WEEKLY;
 import static android.app.usage.UsageStatsManager.INTERVAL_YEARLY;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.usage.ConfigurationStats;
 import android.app.usage.EventList;
 import android.app.usage.EventStats;
@@ -365,43 +367,46 @@
     private static final StatCombiner<UsageStats> sUsageStatsCombiner =
             new StatCombiner<UsageStats>() {
                 @Override
-                public void combine(IntervalStats stats, boolean mutable,
+                public boolean combine(IntervalStats stats, boolean mutable,
                                     List<UsageStats> accResult) {
                     if (!mutable) {
                         accResult.addAll(stats.packageStats.values());
-                        return;
+                        return true;
                     }
 
                     final int statCount = stats.packageStats.size();
                     for (int i = 0; i < statCount; i++) {
                         accResult.add(new UsageStats(stats.packageStats.valueAt(i)));
                     }
+                    return true;
                 }
             };
 
     private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner =
             new StatCombiner<ConfigurationStats>() {
                 @Override
-                public void combine(IntervalStats stats, boolean mutable,
+                public boolean combine(IntervalStats stats, boolean mutable,
                                     List<ConfigurationStats> accResult) {
                     if (!mutable) {
                         accResult.addAll(stats.configurations.values());
-                        return;
+                        return true;
                     }
 
                     final int configCount = stats.configurations.size();
                     for (int i = 0; i < configCount; i++) {
                         accResult.add(new ConfigurationStats(stats.configurations.valueAt(i)));
                     }
+                    return true;
                 }
             };
 
     private static final StatCombiner<EventStats> sEventStatsCombiner =
             new StatCombiner<EventStats>() {
                 @Override
-                public void combine(IntervalStats stats, boolean mutable,
+                public boolean combine(IntervalStats stats, boolean mutable,
                         List<EventStats> accResult) {
                     stats.addEventStatsTo(accResult);
+                    return true;
                 }
             };
 
@@ -414,6 +419,7 @@
      * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
      * provided to select the stats to use from the IntervalStats object.
      */
+    @Nullable
     private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
             StatCombiner<T> combiner) {
         if (intervalType == INTERVAL_BEST) {
@@ -510,16 +516,16 @@
         List<Event> results = queryStats(INTERVAL_DAILY,
                 beginTime, endTime, new StatCombiner<Event>() {
                     @Override
-                    public void combine(IntervalStats stats, boolean mutable,
+                    public boolean combine(IntervalStats stats, boolean mutable,
                             List<Event> accumulatedResult) {
                         final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
                         final int size = stats.events.size();
                         for (int i = startIndex; i < size; i++) {
-                            if (stats.events.get(i).mTimeStamp >= endTime) {
-                                return;
+                            Event event = stats.events.get(i);
+                            if (event.mTimeStamp >= endTime) {
+                                return false;
                             }
 
-                            Event event = stats.events.get(i);
                             final int eventType = event.mEventType;
                             if (eventType == Event.SHORTCUT_INVOCATION
                                     && (flags & HIDE_SHORTCUT_EVENTS) == HIDE_SHORTCUT_EVENTS) {
@@ -552,6 +558,7 @@
                             }
                             accumulatedResult.add(event);
                         }
+                        return true;
                     }
                 });
 
@@ -564,6 +571,60 @@
         return new UsageEvents(results, table, true);
     }
 
+    /**
+     * Returns a {@link UsageEvents} object whose events list contains only the earliest event seen
+     * for each app as well as the earliest event of {@code eventType} seen for each app.
+     */
+    @Nullable
+    UsageEvents queryEarliestAppEvents(final long beginTime, final long endTime,
+            final int eventType) {
+        if (!validRange(checkAndGetTimeLocked(), beginTime, endTime)) {
+            return null;
+        }
+        final ArraySet<String> names = new ArraySet<>();
+        final ArraySet<String> eventSuccess = new ArraySet<>();
+        final List<Event> results = queryStats(INTERVAL_DAILY,
+                beginTime, endTime, (stats, mutable, accumulatedResult) -> {
+                    final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
+                    final int size = stats.events.size();
+                    for (int i = startIndex; i < size; i++) {
+                        final Event event = stats.events.get(i);
+                        if (event.getTimeStamp() >= endTime) {
+                            return false;
+                        }
+                        if (event.getPackageName() == null) {
+                            continue;
+                        }
+                        if (eventSuccess.contains(event.getPackageName())) {
+                            continue;
+                        }
+
+                        final boolean firstEvent = names.add(event.getPackageName());
+
+                        if (event.getEventType() == eventType) {
+                            accumulatedResult.add(event);
+                            eventSuccess.add(event.getPackageName());
+                        } else if (firstEvent) {
+                            // Save the earliest found event for the app, even if it doesn't match.
+                            accumulatedResult.add(event);
+                        }
+                    }
+                    return true;
+                });
+
+        if (results == null || results.isEmpty()) {
+            return null;
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "Found " + results.size() + " early events for " + names.size() + " apps");
+        }
+
+        String[] table = names.toArray(new String[names.size()]);
+        Arrays.sort(table);
+        return new UsageEvents(results, table, false);
+    }
+
+    @Nullable
     UsageEvents queryEventsForPackage(final long beginTime, final long endTime,
             final String packageName, boolean includeTaskRoot) {
         if (!validRange(checkAndGetTimeLocked(), beginTime, endTime)) {
@@ -576,11 +637,11 @@
                     final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
                     final int size = stats.events.size();
                     for (int i = startIndex; i < size; i++) {
-                        if (stats.events.get(i).mTimeStamp >= endTime) {
-                            return;
+                        final Event event = stats.events.get(i);
+                        if (event.mTimeStamp >= endTime) {
+                            return false;
                         }
 
-                        final Event event = stats.events.get(i);
                         if (!packageName.equals(event.mPackage)) {
                             continue;
                         }
@@ -595,6 +656,7 @@
                         }
                         accumulatedResult.add(event);
                     }
+                    return true;
                 });
 
         if (results == null || results.isEmpty()) {
@@ -606,6 +668,48 @@
         return new UsageEvents(results, table, includeTaskRoot);
     }
 
+    /**
+     * Returns a {@link UsageEvents} object whose events list contains only the earliest event seen
+     * for the package as well as the earliest event of {@code eventType} seen for the package.
+     */
+    @Nullable
+    UsageEvents queryEarliestEventsForPackage(final long beginTime, final long endTime,
+            @NonNull final String packageName, final int eventType) {
+        if (!validRange(checkAndGetTimeLocked(), beginTime, endTime)) {
+            return null;
+        }
+        final List<Event> results = queryStats(INTERVAL_DAILY,
+                beginTime, endTime, (stats, mutable, accumulatedResult) -> {
+                    final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
+                    final int size = stats.events.size();
+                    for (int i = startIndex; i < size; i++) {
+                        final Event event = stats.events.get(i);
+                        if (event.getTimeStamp() >= endTime) {
+                            return false;
+                        }
+
+                        if (!packageName.equals(event.getPackageName())) {
+                            continue;
+                        }
+                        if (event.getEventType() == eventType) {
+                            accumulatedResult.add(event);
+                            // We've found the earliest of eventType. No need to keep going.
+                            return false;
+                        } else if (accumulatedResult.size() == 0) {
+                            // Save the earliest found event, even if it doesn't match.
+                            accumulatedResult.add(event);
+                        }
+                    }
+                    return true;
+                });
+
+        if (results == null || results.isEmpty()) {
+            return null;
+        }
+
+        return new UsageEvents(results, new String[]{packageName}, false);
+    }
+
     void persistActiveStats() {
         if (mStatsChanged) {
             Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
@@ -876,7 +980,6 @@
         return Long.toString(elapsedTime);
     }
 
-
     void printEvent(IndentingPrintWriter pw, Event event, boolean prettyDates) {
         pw.printPair("time", formatDateTime(event.mTimeStamp, prettyDates));
         pw.printPair("type", eventToString(event.mEventType));
@@ -925,13 +1028,13 @@
         List<Event> events = queryStats(INTERVAL_DAILY,
                 beginTime, endTime, new StatCombiner<Event>() {
                     @Override
-                    public void combine(IntervalStats stats, boolean mutable,
+                    public boolean combine(IntervalStats stats, boolean mutable,
                             List<Event> accumulatedResult) {
                         final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
                         final int size = stats.events.size();
                         for (int i = startIndex; i < size; i++) {
                             if (stats.events.get(i).mTimeStamp >= endTime) {
-                                return;
+                                return false;
                             }
 
                             Event event = stats.events.get(i);
@@ -940,6 +1043,7 @@
                             }
                             accumulatedResult.add(event);
                         }
+                        return true;
                     }
                 });
 
diff --git a/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java b/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java
index 7e8a134..f695cbd 100644
--- a/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java
+++ b/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java
@@ -62,12 +62,13 @@
     private static final StatCombiner<UsageEvents.Event> sUsageStatsCombiner =
             new StatCombiner<UsageEvents.Event>() {
                 @Override
-                public void combine(IntervalStats stats, boolean mutable,
+                public boolean combine(IntervalStats stats, boolean mutable,
                         List<UsageEvents.Event> accResult) {
                     final int size = stats.events.size();
                     for (int i = 0; i < size; i++) {
                         accResult.add(stats.events.get(i));
                     }
+                    return true;
                 }
             };