Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cc590ac9 authored by Kweku Adams's avatar Kweku Adams
Browse files

Add basic launch time prediction.

We estimate the next app launch time by looking at the past 7 days of
usage history and assuming that the user opens the app like clockwork.
If there is at least 24 hours of usage events, then we take the earliest
ACTIVITY_RESUMED event and estimate that the app will be launched
exactly 7 days after that event. If there is less than 24 hours of
history (which would be the case for a new app), then we take the
earliest ACTIVITY_RESUMED and add 24 hours. If we don't see any launch
event in the past 7 days, then we just say the app should be launched
within a year. If we have a long estimate for an app and it is launched,
then we re-evaluate our estimate because we can now estimate a launch
within the next 7 days.

Bug: 194532703
Test: atest FrameworksMockingServicesTests:PrefetchControllerTest
Test: manually launch apps and check dumpsys for expected launch time changes
Change-Id: I9ef5fc3e3df3c2d029243b1fb8949a4bf21900db
parent 77b40ddf
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
package com.android.server.usage;

import android.annotation.CurrentTimeMillisLong;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.usage.AppStandbyInfo;
@@ -71,6 +72,16 @@ public interface AppStandbyInternal {

    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
+121 −5
Original line number Diff line number Diff line
@@ -25,8 +25,12 @@ import static com.android.server.job.controllers.Package.packageToString;
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 android.util.TimeUtils;

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,10 @@ public class PrefetchController extends StateController {
            || Log.isLoggable(TAG, Log.DEBUG);

    private final PcConstants mPcConstants;
    private final PcHandler mHandler;

    @GuardedBy("mLock")
    private final UsageStatsManagerInternal mUsageStatsManagerInternal;

    @GuardedBy("mLock")
    private final SparseArrayMap<String, ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
@@ -72,11 +82,34 @@ public class PrefetchController extends StateController {
    @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 +179,14 @@ public class PrefetchController extends StateController {
    @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 +206,42 @@ public class PrefetchController extends StateController {
        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 +361,49 @@ public class PrefetchController extends StateController {
        }
    }

    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 +481,8 @@ public class PrefetchController extends StateController {
                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,
+46 −0
Original line number Diff line number Diff line
@@ -32,6 +32,8 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;

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 @@ public class AppIdleHistory {
    // 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 @@ public class AppIdleHistory {
        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
@@ -410,6 +417,17 @@ public class AppIdleHistory {
        app.lastPredictedBucket = bucket;
    }

    /**
     * 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.
@@ -442,6 +460,23 @@ public class AppIdleHistory {
        appUsageHistory.lastRestrictReason = reason;
    }

    /**
     * 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
@@ -671,6 +706,8 @@ public class AppIdleHistory {
                                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 @@ public class AppIdleHistory {
                }
                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 @@ public class AppIdleHistory {
        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 @@ public class AppIdleHistory {
                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();
        }
+19 −0
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
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;
@@ -1086,6 +1087,24 @@ public class AppStandbyController
        }
    }

    @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();
+5 −0
Original line number Diff line number Diff line
@@ -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 @@ public final class UsageEvents implements Parcelable {
         * <p/>
         * See {@link System#currentTimeMillis()}.
         */
        @CurrentTimeMillisLong
        public long getTimeStamp() {
            return mTimeStamp;
        }
@@ -801,6 +803,9 @@ public final class UsageEvents implements Parcelable {
     * @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;
        }
Loading