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 968c6e5d9cf6..0f36d32c459c 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 @@ public interface AppStandbyInternal { 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 @@ 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 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 6232dfb12822..393f368920b7 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 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,9 @@ public class PrefetchController extends StateController { || Log.isLoggable(TAG, Log.DEBUG); private final PcConstants mPcConstants; + private final PcHandler mHandler; + + private final UsageStatsManagerInternal mUsageStatsManagerInternal; @GuardedBy("mLock") private final SparseArrayMap> mTrackedJobs = new SparseArrayMap<>(); @@ -72,11 +81,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 +178,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 +205,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 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 @@ 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 +480,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, 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 187422bf1970..8b175127373a 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 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 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 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 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(); } 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 096211b433c7..abbae4e8e43c 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 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(); diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java index 71fae3debbea..0f5cd4e44bed 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 @@ public final class UsageEvents implements Parcelable { *

* 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; } diff --git a/services/core/java/android/app/usage/UsageStatsManagerInternal.java b/services/core/java/android/app/usage/UsageStatsManagerInternal.java index b2226d1e0fa3..21fc19ec3079 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 class UsageStatsManagerInternal { 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 @@ public abstract class UsageStatsManagerInternal { /** 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 98e089e0ab94..e5b2d141ba10 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.HOUR_IN_MILLIS; 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.After; 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 @@ import java.util.concurrent.Executor; @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 @@ public class PrefetchControllerTest { // 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) invocationOnMock -> null) .when(() -> DeviceConfig.addOnPropertiesChangedListener( @@ -93,7 +129,7 @@ public class PrefetchControllerTest { // 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 @@ public class PrefetchControllerTest { // Initialize real objects. // Capture the listeners. + ArgumentCaptor 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 @@ public class PrefetchControllerTest { } } + 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 @@ public class PrefetchControllerTest { 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 @@ public class PrefetchControllerTest { 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 24c58f49bed6..1542b01be8e1 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 @@ package com.android.server.usage; 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.app.usage.UsageEvents.Event; 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 @@ import java.util.HashMap; @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 @@ public class UserUsageStatsServiceTest { private UserUsageStatsService mService; private MockitoSession mMockitoSession; + private File mDir; + @Mock private Context mContext; @Mock @@ -66,8 +74,11 @@ public class UserUsageStatsServiceTest { .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 installedPkgs = new HashMap<>(); installedPkgs.put(TEST_PACKAGE_NAME, System.currentTimeMillis()); @@ -77,6 +88,9 @@ public class UserUsageStatsServiceTest { @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 @@ public class UserUsageStatsServiceTest { 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 @@ public class UserUsageStatsServiceTest { 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 @@ public class UserUsageStatsServiceTest { } 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 b2dacab26365..75bd2ccbe635 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 @@ public class UsageStatsDatabaseTest { private static final UsageStatsDatabase.StatCombiner mIntervalStatsVerifier = new UsageStatsDatabase.StatCombiner() { @Override - public void combine(IntervalStats stats, boolean mutable, + public boolean combine(IntervalStats stats, boolean mutable, List 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 ad042dddf03d..cc33f88d396d 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 @@ public class UsageStatsDatabase { * @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 accumulatedResult); + boolean combine(IntervalStats stats, boolean mutable, List accumulatedResult); } /** @@ -863,8 +865,9 @@ public class UsageStatsDatabase { 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 f0ceff15c763..6dbf4c56da3b 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_STOPPED; 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.Slog; 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.internal.util.IndentingPrintWriter; 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 @@ public class UsageStatsService extends SystemService implements 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 @@ public class UsageStatsService extends SystemService implements 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 @@ public class UsageStatsService extends SystemService implements private final SparseArray> mReportedEvents = new SparseArray<>(); final SparseArray> mUsageReporters = new SparseArray(); final SparseArray mVisibleActivities = new SparseArray(); + @GuardedBy("mLock") + private final SparseArray mLaunchTimeAlarmQueues = new SparseArray<>(); private final ArraySet mUsageEventListeners = new ArraySet<>(); + private final CopyOnWriteArraySet + mEstimatedLaunchTimeChangedListeners = new CopyOnWriteArraySet<>(); private static class ActivityData { private final String mTaskRootPackage; @@ -369,6 +387,11 @@ public class UsageStatsService extends SystemService implements } 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 @@ public class UsageStatsService extends SystemService implements } 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 @@ public class UsageStatsService extends SystemService implements *
* Note: DO NOT call this while holding the usage stats lock ({@code mLock}). */ + @Nullable private HashMap getInstalledPackages(int userId) { if (mPackageManager == null) { return null; @@ -472,6 +498,33 @@ public class UsageStatsService extends SystemService implements } } + private class LaunchTimeAlarmQueue extends AlarmQueue { + 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 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 @@ public class UsageStatsService extends SystemService implements @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 @@ public class UsageStatsService extends SystemService implements /** * 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 @@ public class UsageStatsService extends SystemService implements 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 @@ public class UsageStatsService extends SystemService implements 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 @@ public class UsageStatsService extends SystemService implements // 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 @@ public class UsageStatsService extends SystemService implements /** * Called by the Binder stub. */ + @Nullable UsageEvents queryEventsForPackage(int userId, long beginTime, long endTime, String packageName, boolean includeTaskRoot) { synchronized (mLock) { @@ -1290,6 +1372,183 @@ public class UsageStatsService extends SystemService implements } } + @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 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 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 @@ public class UsageStatsService extends SystemService implements } } + /** + * 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 @@ public class UsageStatsService extends SystemService implements 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 pkgNames = (ArraySet) 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; @@ -2462,6 +2775,11 @@ public class UsageStatsService extends SystemService implements mAppStandby.setLastJobRunTime(packageName, userId, elapsedRealtime); } + @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 class UsageStatsService extends SystemService implements 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 c4a8e8148d22..23694fc5b418 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_MONTHLY; 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 @@ class UserUsageStatsService { private static final StatCombiner sUsageStatsCombiner = new StatCombiner() { @Override - public void combine(IntervalStats stats, boolean mutable, + public boolean combine(IntervalStats stats, boolean mutable, List 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 sConfigStatsCombiner = new StatCombiner() { @Override - public void combine(IntervalStats stats, boolean mutable, + public boolean combine(IntervalStats stats, boolean mutable, List 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 sEventStatsCombiner = new StatCombiner() { @Override - public void combine(IntervalStats stats, boolean mutable, + public boolean combine(IntervalStats stats, boolean mutable, List accResult) { stats.addEventStatsTo(accResult); + return true; } }; @@ -414,6 +419,7 @@ class UserUsageStatsService { * 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 List queryStats(int intervalType, final long beginTime, final long endTime, StatCombiner combiner) { if (intervalType == INTERVAL_BEST) { @@ -510,16 +516,16 @@ class UserUsageStatsService { List results = queryStats(INTERVAL_DAILY, beginTime, endTime, new StatCombiner() { @Override - public void combine(IntervalStats stats, boolean mutable, + public boolean combine(IntervalStats stats, boolean mutable, List 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 @@ class UserUsageStatsService { } accumulatedResult.add(event); } + return true; } }); @@ -564,6 +571,60 @@ class UserUsageStatsService { 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 names = new ArraySet<>(); + final ArraySet eventSuccess = new ArraySet<>(); + final List 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 @@ class UserUsageStatsService { 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 @@ class UserUsageStatsService { } accumulatedResult.add(event); } + return true; }); if (results == null || results.isEmpty()) { @@ -606,6 +668,48 @@ class UserUsageStatsService { 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 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 @@ class UserUsageStatsService { 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 @@ class UserUsageStatsService { List events = queryStats(INTERVAL_DAILY, beginTime, endTime, new StatCombiner() { @Override - public void combine(IntervalStats stats, boolean mutable, + public boolean combine(IntervalStats stats, boolean mutable, List 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 @@ class UserUsageStatsService { } 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 7e8a13470c35..f695cbd5daf9 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 @@ public class UsageStatsDatabasePerfTest { private static final StatCombiner sUsageStatsCombiner = new StatCombiner() { @Override - public void combine(IntervalStats stats, boolean mutable, + public boolean combine(IntervalStats stats, boolean mutable, List accResult) { final int size = stats.events.size(); for (int i = 0; i < size; i++) { accResult.add(stats.events.get(i)); } + return true; } };