OSDN Git Service

Throttle jobs for idle apps
authorAmith Yamasani <yamasani@google.com>
Wed, 4 Mar 2015 17:56:14 +0000 (09:56 -0800)
committerAmith Yamasani <yamasani@google.com>
Fri, 3 Apr 2015 20:20:19 +0000 (13:20 -0700)
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

core/java/android/app/usage/UsageStatsManagerInternal.java
core/java/android/provider/Settings.java
services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
services/core/java/com/android/server/am/ActivityManagerService.java
services/core/java/com/android/server/job/JobSchedulerService.java
services/core/java/com/android/server/job/controllers/AppIdleController.java [new file with mode: 0644]
services/core/java/com/android/server/job/controllers/JobStatus.java
services/core/java/com/android/server/job/controllers/StateController.java
services/usage/java/com/android/server/usage/UsageStatsService.java
services/usage/java/com/android/server/usage/UserUsageStatsService.java

index 0122069..8b3fc2e 100644 (file)
@@ -57,4 +57,41 @@ public abstract class UsageStatsManagerInternal {
      * Prepares the UsageStatsService for shutdown.
      */
     public abstract void prepareShutdown();
+
+    /**
+     * Returns true if the app has not been used for a certain amount of time. How much time?
+     * Could be hours, could be days, who knows?
+     *
+     * @param packageName
+     * @param userId
+     * @return
+     */
+    public abstract boolean isAppIdle(String packageName, int userId);
+
+    /**
+     * Returns the most recent time that the specified package was active for the given user.
+     * @param packageName The package to search.
+     * @param userId The user id of the user of interest.
+     * @return The timestamp of when the package was last used, or -1 if it hasn't been used.
+     */
+    public abstract long getLastPackageAccessTime(String packageName, int userId);
+
+    /**
+     * Sets up a listener for changes to packages being accessed.
+     * @param listener A listener within the system process.
+     */
+    public abstract void addAppIdleStateChangeListener(
+            AppIdleStateChangeListener listener);
+
+    /**
+     * Removes a listener that was previously added for package usage state changes.
+     * @param listener The listener within the system process to remove.
+     */
+    public abstract void removeAppIdleStateChangeListener(
+            AppIdleStateChangeListener listener);
+
+    public interface AppIdleStateChangeListener {
+        void onAppIdleStateChanged(String packageName, int userId, boolean idle);
+    }
+
 }
index ec7e8b2..f488ea0 100644 (file)
@@ -3026,7 +3026,7 @@ public final class Settings {
         };
 
         /**
-         * These are all pulbic system settings
+         * These are all public system settings
          *
          * @hide
          */
@@ -3126,7 +3126,7 @@ public final class Settings {
         }
 
         /**
-         * These are all pulbic system settings
+         * These are all public system settings
          *
          * @hide
          */
@@ -5363,6 +5363,13 @@ public final class Settings {
         public static final String SLEEP_TIMEOUT = "sleep_timeout";
 
         /**
+         * Duration in milliseconds that an app should be inactive before it is considered idle.
+         * <p/>Type: Long
+         * @hide
+         */
+        public static final String APP_IDLE_DURATION = "app_idle_duration";
+
+        /**
          * This are the settings to be backed up.
          *
          * NOTE: Settings are backed up and restored in the order they appear
@@ -5425,6 +5432,7 @@ public final class Settings {
          * since the managed profile doesn't get to change them.
          */
         private static final Set<String> CLONE_TO_MANAGED_PROFILE = new ArraySet<>();
+
         static {
             CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_ENABLED);
             CLONE_TO_MANAGED_PROFILE.add(ALLOW_MOCK_LOCATION);
index 7a74e45..c2b0a4d 100644 (file)
@@ -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" : "";
index b0b410b..cdaa5a3 100644 (file)
@@ -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) {
index d79b5fd..ecda36a 100644 (file)
@@ -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 (file)
index 0000000..03e9ad5
--- /dev/null
@@ -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<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
+    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();
+        }
+    }
+}
index e3c55b6..69c63f3 100644 (file)
@@ -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 <strong>or</strong> 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)" : "")
                 + "]";
     }
index efd1928..cda7c32 100644 (file)
@@ -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.
      */
index 5eefe6a..f458dbc 100644 (file)
@@ -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<UsageStatsManagerInternal.AppIdleStateChangeListener>
+            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);
+        }
     }
 }
index 75fa030..afe27c7 100644 (file)
@@ -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");