From b0ff32245cb6b51e43dd3ee40b86d683c62de2b9 Mon Sep 17 00:00:00 2001 From: Amith Yamasani Date: Wed, 4 Mar 2015 09:56:14 -0800 Subject: Throttle jobs for idle apps First pass at delaying jobs from apps that are idle. TODO: Throttle syncs TODO: Provide a periodic point at which apps are checked for idleness. Apps that switch to foreground process state are tracked by UsageStats as an INTERACTION event that affects the last-used timestamp. JobScheduler's logic for when an app is ready is trumped by the idleness of the app, and only if the battery is not charging. When charging state changes, we update the idle state of all the tracked jobs. android package is whitelisted. Bug: 20066058 Change-Id: I0a0acb517b100a5c7b11e3f435f4141375f3451f --- .../server/am/ActivityManagerDebugConfig.java | 1 + .../android/server/am/ActivityManagerService.java | 30 +++- .../android/server/job/JobSchedulerService.java | 7 +- .../server/job/controllers/AppIdleController.java | 174 +++++++++++++++++++++ .../android/server/job/controllers/JobStatus.java | 9 +- .../server/job/controllers/StateController.java | 2 +- .../android/server/usage/UsageStatsService.java | 133 ++++++++++++++++ .../server/usage/UserUsageStatsService.java | 17 +- 8 files changed, 365 insertions(+), 8 deletions(-) create mode 100644 services/core/java/com/android/server/job/controllers/AppIdleController.java (limited to 'services') diff --git a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java index 7a74e45..c2b0a4d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java +++ b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java @@ -73,6 +73,7 @@ class ActivityManagerDebugConfig { static final boolean DEBUG_URI_PERMISSION = DEBUG_ALL || false; static final boolean DEBUG_USER_LEAVING = DEBUG_ALL || false; static final boolean DEBUG_VISIBILITY = DEBUG_ALL || false; + static final boolean DEBUG_USAGE_STATS = DEBUG_ALL || true; static final String POSTFIX_BACKUP = (APPEND_CATEGORY_NAME) ? "_Backup" : ""; static final String POSTFIX_BROADCAST = (APPEND_CATEGORY_NAME) ? "_Broadcast" : ""; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b0b410b..cdaa5a3 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -43,6 +43,7 @@ import android.app.ITaskStackListener; import android.app.ProfilerInfo; import android.app.admin.DevicePolicyManager; import android.app.usage.UsageEvents; +import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; import android.appwidget.AppWidgetManager; import android.content.res.Resources; @@ -61,8 +62,8 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.SparseIntArray; - import android.view.Display; + import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.DumpHeapActivity; @@ -96,7 +97,6 @@ import com.android.server.pm.UserManagerService; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.wm.AppTransition; import com.android.server.wm.WindowManagerService; - import com.google.android.collect.Lists; import com.google.android.collect.Maps; @@ -17854,6 +17854,10 @@ public final class ActivityManagerService extends ActivityManagerNative app.lastCpuTime = app.curCpuTime; } + // Inform UsageStats of important process state change + // Must be called before updating setProcState + maybeUpdateUsageStats(app); + app.setProcState = app.curProcState; if (app.setProcState >= ActivityManager.PROCESS_STATE_HOME) { app.notCachedSinceIdle = false; @@ -17916,6 +17920,28 @@ public final class ActivityManagerService extends ActivityManagerNative return success; } + private void maybeUpdateUsageStats(ProcessRecord app) { + if (DEBUG_USAGE_STATS) { + Slog.d(TAG, "Checking proc [" + Arrays.toString(app.getPackageList()) + + "] state changes: old = " + app.setProcState + ", new = " + + app.curProcState); + } + if (mUsageStatsService == null) { + return; + } + if (app.curProcState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND + && (app.setProcState > ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND + || app.setProcState < 0)) { + String[] packages = app.getPackageList(); + if (packages != null) { + for (int i = 0; i < packages.length; i++) { + mUsageStatsService.reportEvent(packages[i], app.userId, + UsageEvents.Event.INTERACTION); + } + } + } + } + private final void setProcessTrackerStateLocked(ProcessRecord proc, int memFactor, long now) { if (proc.thread != null) { if (proc.baseProcessTracker != null) { diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index d79b5fd..ecda36a 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -51,6 +51,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.app.IBatteryStats; +import com.android.server.job.controllers.AppIdleController; import com.android.server.job.controllers.BatteryController; import com.android.server.job.controllers.ConnectivityController; import com.android.server.job.controllers.IdleController; @@ -317,6 +318,7 @@ public class JobSchedulerService extends com.android.server.SystemService mControllers.add(TimeController.get(this)); mControllers.add(IdleController.get(this)); mControllers.add(BatteryController.get(this)); + mControllers.add(AppIdleController.get(this)); mHandler = new JobHandler(context.getMainLooper()); mJobSchedulerStub = new JobSchedulerStub(); @@ -688,7 +690,6 @@ public class JobSchedulerService extends com.android.server.SystemService final boolean jobPending = mPendingJobs.contains(job); final boolean jobActive = isCurrentlyActiveLocked(job); final boolean userRunning = mStartedUsers.contains(job.getUserId()); - if (DEBUG) { Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + " ready=" + jobReady + " pending=" + jobPending @@ -738,6 +739,10 @@ public class JobSchedulerService extends com.android.server.SystemService } } if (availableContext != null) { + if (DEBUG) { + Slog.d(TAG, "About to run job " + + nextPending.getJob().getService().toString()); + } if (!availableContext.executeRunnableJob(nextPending)) { if (DEBUG) { Slog.d(TAG, "Error executing " + nextPending); diff --git a/services/core/java/com/android/server/job/controllers/AppIdleController.java b/services/core/java/com/android/server/job/controllers/AppIdleController.java new file mode 100644 index 0000000..03e9ad5 --- /dev/null +++ b/services/core/java/com/android/server/job/controllers/AppIdleController.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import android.app.usage.UsageStatsManagerInternal; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.BatteryManagerInternal; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateChangedListener; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Controls when apps are considered idle and if jobs pertaining to those apps should + * be executed. Apps that haven't been actively launched or accessed from a foreground app + * for a certain amount of time (maybe hours or days) are considered idle. When the app comes + * out of idle state, it will be allowed to run scheduled jobs. + */ +public class AppIdleController extends StateController + implements UsageStatsManagerInternal.AppIdleStateChangeListener { + + private static final String LOG_TAG = "AppIdleController"; + private static final boolean DEBUG = true; + + // Singleton factory + private static Object sCreationLock = new Object(); + private static volatile AppIdleController sController; + final ArrayList mTrackedTasks = new ArrayList(); + private final UsageStatsManagerInternal mUsageStatsInternal; + private final BatteryManagerInternal mBatteryManagerInternal; + private boolean mPluggedIn; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) { + int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + // TODO: Allow any charger type + onPluggedIn((plugged & BatteryManager.BATTERY_PLUGGED_AC) != 0); + } + } + }; + + public static AppIdleController get(JobSchedulerService service) { + synchronized (sCreationLock) { + if (sController == null) { + sController = new AppIdleController(service, service.getContext()); + } + return sController; + } + } + + private AppIdleController(StateChangedListener stateChangedListener, Context context) { + super(stateChangedListener, context); + mUsageStatsInternal = LocalServices.getService(UsageStatsManagerInternal.class); + mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class); + mPluggedIn = isPowered(); + mUsageStatsInternal.addAppIdleStateChangeListener(this); + registerReceivers(); + } + + private void registerReceivers() { + // Monitor battery charging state + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + mContext.registerReceiver(mReceiver, filter); + } + + private boolean isPowered() { + // TODO: Allow any charger type + return mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_AC); + } + + @Override + public void maybeStartTrackingJob(JobStatus jobStatus) { + synchronized (mTrackedTasks) { + mTrackedTasks.add(jobStatus); + String packageName = jobStatus.job.getService().getPackageName(); + final boolean appIdle = !mPluggedIn && mUsageStatsInternal.isAppIdle(packageName, + jobStatus.getUserId()); + if (DEBUG) { + Slog.d(LOG_TAG, "Start tracking, setting idle state of " + + packageName + " to " + appIdle); + } + jobStatus.appNotIdleConstraintSatisfied.set(!appIdle); + } + } + + @Override + public void maybeStopTrackingJob(JobStatus jobStatus) { + synchronized (mTrackedTasks) { + mTrackedTasks.remove(jobStatus); + } + } + + @Override + public void dumpControllerState(PrintWriter pw) { + // TODO: + } + + @Override + public void onAppIdleStateChanged(String packageName, int userId, boolean idle) { + boolean changed = false; + synchronized (mTrackedTasks) { + // If currently plugged in, we don't care about app idle state + if (mPluggedIn) { + return; + } + for (JobStatus task : mTrackedTasks) { + if (task.job.getService().getPackageName().equals(packageName) + && task.getUserId() == userId) { + if (task.appNotIdleConstraintSatisfied.get() != !idle) { + if (DEBUG) { + Slog.d(LOG_TAG, "App Idle state changed, setting idle state of " + + packageName + " to " + idle); + } + task.appNotIdleConstraintSatisfied.set(!idle); + changed = true; + } + } + } + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + + void onPluggedIn(boolean pluggedIn) { + // Flag if any app's idle state has changed + boolean changed = false; + synchronized (mTrackedTasks) { + if (mPluggedIn == pluggedIn) { + return; + } + mPluggedIn = pluggedIn; + for (JobStatus task : mTrackedTasks) { + String packageName = task.job.getService().getPackageName(); + final boolean appIdle = !mPluggedIn && mUsageStatsInternal.isAppIdle(packageName, + task.getUserId()); + if (DEBUG) { + Slog.d(LOG_TAG, "Plugged in " + pluggedIn + ", setting idle state of " + + packageName + " to " + appIdle); + } + if (task.appNotIdleConstraintSatisfied.get() == appIdle) { + task.appNotIdleConstraintSatisfied.set(!appIdle); + changed = true; + } + } + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } +} diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index e3c55b6..69c63f3 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -54,6 +54,7 @@ public class JobStatus { final AtomicBoolean idleConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean unmeteredConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean connectivityConstraintSatisfied = new AtomicBoolean(); + final AtomicBoolean appNotIdleConstraintSatisfied = new AtomicBoolean(); /** * Earliest point in the future at which this job will be eligible to run. A value of 0 @@ -199,8 +200,11 @@ public class JobStatus { * the constraints are satisfied or the deadline on the job has expired. */ public synchronized boolean isReady() { - return isConstraintsSatisfied() - || (hasDeadlineConstraint() && deadlineConstraintSatisfied.get()); + // Deadline constraint trumps other constraints + // AppNotIdle implicit constraint trumps all! + return (isConstraintsSatisfied() + || (hasDeadlineConstraint() && deadlineConstraintSatisfied.get())) + && appNotIdleConstraintSatisfied.get(); } /** @@ -229,6 +233,7 @@ public class JobStatus { + ",N=" + job.getNetworkType() + ",C=" + job.isRequireCharging() + ",I=" + job.isRequireDeviceIdle() + ",F=" + numFailures + ",P=" + job.isPersisted() + + ",ANI=" + appNotIdleConstraintSatisfied.get() + (isReady() ? "(READY)" : "") + "]"; } diff --git a/services/core/java/com/android/server/job/controllers/StateController.java b/services/core/java/com/android/server/job/controllers/StateController.java index efd1928..cda7c32 100644 --- a/services/core/java/com/android/server/job/controllers/StateController.java +++ b/services/core/java/com/android/server/job/controllers/StateController.java @@ -44,7 +44,7 @@ public abstract class StateController { /** * Implement the logic here to decide whether a job should be tracked by this controller. - * This logic is put here so the JobManger can be completely agnostic of Controller logic. + * This logic is put here so the JobManager can be completely agnostic of Controller logic. * Also called when updating a task, so implementing controllers have to be aware of * preexisting tasks. */ diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 5eefe6a..f458dbc 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -21,8 +21,10 @@ import android.app.AppOpsManager; import android.app.usage.ConfigurationStats; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageEvents; +import android.app.usage.UsageEvents.Event; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; +import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -32,6 +34,8 @@ import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Binder; import android.os.Environment; import android.os.Handler; @@ -42,6 +46,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; @@ -53,6 +58,7 @@ import com.android.server.SystemService; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -74,6 +80,7 @@ public class UsageStatsService extends SystemService implements static final int MSG_REPORT_EVENT = 0; static final int MSG_FLUSH_TO_DISK = 1; static final int MSG_REMOVE_USER = 2; + static final int MSG_INFORM_LISTENERS = 3; private final Object mLock = new Object(); Handler mHandler; @@ -85,6 +92,12 @@ public class UsageStatsService extends SystemService implements long mRealTimeSnapshot; long mSystemTimeSnapshot; + private static final long DEFAULT_APP_IDLE_THRESHOLD_MILLIS = 3L * 24 * 60 * 60 * 1000; //3 days + private long mAppIdleDurationMillis; + + private ArrayList + mPackageAccessListeners = new ArrayList<>(); + public UsageStatsService(Context context) { super(context); } @@ -112,11 +125,24 @@ public class UsageStatsService extends SystemService implements mRealTimeSnapshot = SystemClock.elapsedRealtime(); mSystemTimeSnapshot = System.currentTimeMillis(); + // Look at primary user's secure setting for this. TODO: Maybe apply different + // thresholds for different users. + mAppIdleDurationMillis = Settings.Secure.getLongForUser(getContext().getContentResolver(), + Settings.Secure.APP_IDLE_DURATION, DEFAULT_APP_IDLE_THRESHOLD_MILLIS, + UserHandle.USER_OWNER); publishLocalService(UsageStatsManagerInternal.class, new LocalService()); publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService()); } + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + // Observe changes to the threshold + new SettingsObserver(mHandler).registerObserver(); + } + } + private class UserRemovedReceiver extends BroadcastReceiver { @Override @@ -235,7 +261,19 @@ public class UsageStatsService extends SystemService implements final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow); + final long lastUsed = service.getLastPackageAccessTime(event.mPackage); + final boolean previouslyIdle = hasPassedIdleDuration(lastUsed); service.reportEvent(event); + // Inform listeners if necessary + if ((event.mEventType == Event.MOVE_TO_FOREGROUND + || event.mEventType == Event.MOVE_TO_BACKGROUND + || event.mEventType == Event.INTERACTION)) { + if (previouslyIdle) { + // Slog.d(TAG, "Informing listeners of out-of-idle " + event.mPackage); + mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId, + /* idle = */ 0, event.mPackage)); + } + } } } @@ -308,6 +346,53 @@ public class UsageStatsService extends SystemService implements } } + /** + * Called by LocalService stub. + */ + long getLastPackageAccessTime(String packageName, int userId) { + synchronized (mLock) { + final long timeNow = checkAndGetTimeLocked(); + // android package is always considered non-idle. + // TODO: Add a generic whitelisting mechanism + if (packageName.equals("android")) { + return timeNow; + } + final UserUsageStatsService service = + getUserDataAndInitializeIfNeededLocked(userId, timeNow); + return service.getLastPackageAccessTime(packageName); + } + } + + void addListener(AppIdleStateChangeListener listener) { + synchronized (mLock) { + if (!mPackageAccessListeners.contains(listener)) { + mPackageAccessListeners.add(listener); + } + } + } + + void removeListener(AppIdleStateChangeListener listener) { + synchronized (mLock) { + mPackageAccessListeners.remove(listener); + } + } + + private boolean hasPassedIdleDuration(long lastUsed) { + final long now = System.currentTimeMillis(); + return lastUsed < now - mAppIdleDurationMillis; + } + + boolean isAppIdle(String packageName, int userId) { + final long lastUsed = getLastPackageAccessTime(packageName, userId); + return hasPassedIdleDuration(lastUsed); + } + + void informListeners(String packageName, int userId, boolean isIdle) { + for (AppIdleStateChangeListener listener : mPackageAccessListeners) { + listener.onAppIdleStateChanged(packageName, userId, isIdle); + } + } + private static boolean validRange(long currentTime, long beginTime, long endTime) { return beginTime <= currentTime && beginTime < endTime; } @@ -366,6 +451,10 @@ public class UsageStatsService extends SystemService implements removeUser(msg.arg1); break; + case MSG_INFORM_LISTENERS: + informListeners((String) msg.obj, msg.arg1, msg.arg2 == 1); + break; + default: super.handleMessage(msg); break; @@ -373,6 +462,29 @@ public class UsageStatsService extends SystemService implements } } + /** + * Observe settings changes for Settings.Secure.APP_IDLE_DURATION. + */ + private class SettingsObserver extends ContentObserver { + + SettingsObserver(Handler handler) { + super(handler); + } + + void registerObserver() { + getContext().getContentResolver().registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.APP_IDLE_DURATION), false, this, UserHandle.USER_OWNER); + } + + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + mAppIdleDurationMillis = Settings.Secure.getLongForUser(getContext().getContentResolver(), + Settings.Secure.APP_IDLE_DURATION, DEFAULT_APP_IDLE_THRESHOLD_MILLIS, + UserHandle.USER_OWNER); + // TODO: Check if we need to update idle states of all the apps + } + } + private class BinderService extends IUsageStatsManager.Stub { private boolean hasPermission(String callingPackage) { @@ -523,11 +635,32 @@ public class UsageStatsService extends SystemService implements } @Override + public boolean isAppIdle(String packageName, int userId) { + return UsageStatsService.this.isAppIdle(packageName, userId); + } + + @Override + public long getLastPackageAccessTime(String packageName, int userId) { + return UsageStatsService.this.getLastPackageAccessTime(packageName, userId); + } + + @Override public void prepareShutdown() { // This method *WILL* do IO work, but we must block until it is finished or else // we might not shutdown cleanly. This is ok to do with the 'am' lock held, because // we are shutting down. shutdown(); } + + @Override + public void addAppIdleStateChangeListener(AppIdleStateChangeListener listener) { + UsageStatsService.this.addListener(listener); + } + + @Override + public void removeAppIdleStateChangeListener( + AppIdleStateChangeListener listener) { + UsageStatsService.this.removeListener(listener); + } } } diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index 75fa030..afe27c7 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -65,7 +65,8 @@ class UserUsageStatsService { void onStatsUpdated(); } - UserUsageStatsService(Context context, int userId, File usageStatsDir, StatsUpdatedListener listener) { + UserUsageStatsService(Context context, int userId, File usageStatsDir, + StatsUpdatedListener listener) { mContext = context; mDailyExpiryDate = new UnixCalendar(0); mDatabase = new UsageStatsDatabase(usageStatsDir); @@ -161,7 +162,9 @@ class UserUsageStatsService { if (currentDailyStats.events == null) { currentDailyStats.events = new TimeSparseArray<>(); } - currentDailyStats.events.put(event.mTimeStamp, event); + if (event.mEventType != UsageEvents.Event.INTERACTION) { + currentDailyStats.events.put(event.mTimeStamp, event); + } for (IntervalStats stats : mCurrentStats) { if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) { @@ -328,6 +331,16 @@ class UserUsageStatsService { return new UsageEvents(results, table); } + long getLastPackageAccessTime(String packageName) { + final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY]; + UsageStats packageUsage; + if ((packageUsage = yearly.packageStats.get(packageName)) == null) { + return -1; + } else { + return packageUsage.getLastTimeUsed(); + } + } + void persistActiveStats() { if (mStatsChanged) { Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); -- cgit v1.1