Merge "Introduce "IdleService" API to expose idle-time maintenance to apps"
diff --git a/Android.mk b/Android.mk
index 781d5d1..0865d24 100644
--- a/Android.mk
+++ b/Android.mk
@@ -89,6 +89,8 @@
 	core/java/android/app/backup/IFullBackupRestoreObserver.aidl \
 	core/java/android/app/backup/IRestoreObserver.aidl \
 	core/java/android/app/backup/IRestoreSession.aidl \
+	core/java/android/app/maintenance/IIdleCallback.aidl \
+	core/java/android/app/maintenance/IIdleService.aidl \
 	core/java/android/bluetooth/IBluetooth.aidl \
 	core/java/android/bluetooth/IBluetoothA2dp.aidl \
 	core/java/android/bluetooth/IBluetoothCallback.aidl \
diff --git a/api/current.txt b/api/current.txt
index 9a97fe4..7673ff3 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -4886,6 +4886,20 @@
 
 }
 
+package android.app.maintenance {
+
+  public abstract class IdleService extends android.app.Service {
+    ctor public IdleService();
+    method public final void finishIdle();
+    method public final android.os.IBinder onBind(android.content.Intent);
+    method public abstract boolean onIdleStart();
+    method public abstract void onIdleStop();
+    field public static final java.lang.String PERMISSION_BIND = "android.permission.BIND_IDLE_SERVICE";
+    field public static final java.lang.String SERVICE_INTERFACE = "android.service.idle.IdleService";
+  }
+
+}
+
 package android.appwidget {
 
   public class AppWidgetHost {
diff --git a/core/java/android/app/maintenance/IIdleCallback.aidl b/core/java/android/app/maintenance/IIdleCallback.aidl
new file mode 100644
index 0000000..582dede
--- /dev/null
+++ b/core/java/android/app/maintenance/IIdleCallback.aidl
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2014, 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 android.app.maintenance;
+
+import android.app.maintenance.IIdleService;
+
+/**
+ * The server side of the idle maintenance IPC protocols.  The app-side implementation
+ * invokes on this interface to indicate completion of the (asynchronous) instructions
+ * issued by the server.
+ *
+ * In all cases, the 'who' parameter is the caller's service binder, used to track
+ * which idle service instance is reporting.
+ *
+ * {@hide}
+ */
+interface IIdleCallback {
+    /**
+     * Acknowledge receipt and processing of the asynchronous "start idle work" incall.
+     * 'result' is true if the app wants some time to perform ongoing background
+     * idle-time work; or false if the app declares that it does not need any time
+     * for such work.
+     */
+    void acknowledgeStart(int token, boolean result);
+
+    /**
+     * Acknowledge receipt and processing of the asynchronous "stop idle work" incall.
+     */
+    void acknowledgeStop(int token);
+
+    /*
+     * Tell the idle service manager that we're done with our idle maintenance, so that
+     * it can go on to the next one and stop attributing wakelock time to us etc.
+     *
+     * @param opToken The identifier passed in the startIdleMaintenance() call that
+     *        indicated the beginning of this service's idle timeslice.
+     */
+    void idleFinished(int token);
+}
diff --git a/core/java/android/app/maintenance/IIdleService.aidl b/core/java/android/app/maintenance/IIdleService.aidl
new file mode 100644
index 0000000..54abccd
--- /dev/null
+++ b/core/java/android/app/maintenance/IIdleService.aidl
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2014, 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 android.app.maintenance;
+
+import android.app.maintenance.IIdleCallback;
+
+/**
+ * Interface that the framework uses to communicate with application code
+ * that implements an idle-time "maintenance" service.  End user code does
+ * not implement this interface directly; instead, the app's idle service
+ * implementation will extend android.app.maintenance.IdleService.
+ * {@hide}
+ */
+oneway interface IIdleService {
+    /**
+     * Begin your idle-time work.
+     */
+    void startIdleMaintenance(IIdleCallback callbackBinder, int token);
+    void stopIdleMaintenance(IIdleCallback callbackBinder, int token);
+}
diff --git a/core/java/android/app/maintenance/IdleService.java b/core/java/android/app/maintenance/IdleService.java
new file mode 100644
index 0000000..2331b81
--- /dev/null
+++ b/core/java/android/app/maintenance/IdleService.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2014 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 android.app.maintenance;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+/**
+ * Idle maintenance API.  Full docs TBW (to be written).
+ */
+public abstract class IdleService extends Service {
+    private static final String TAG = "IdleService";
+
+    static final int MSG_START = 1;
+    static final int MSG_STOP = 2;
+    static final int MSG_FINISH = 3;
+
+    IdleHandler mHandler;
+    IIdleCallback mCallbackBinder;
+    int mToken;
+    final Object mHandlerLock = new Object();
+
+    void ensureHandler() {
+        synchronized (mHandlerLock) {
+            if (mHandler == null) {
+                mHandler = new IdleHandler(getMainLooper());
+            }
+        }
+    }
+
+    /**
+     * TBW: the idle service should supply an intent-filter handling this intent
+     * <p>
+     * <p class="note">The application must also protect the idle service with the
+     * {@code "android.permission.BIND_IDLE_SERVICE"} permission to ensure that other
+     * applications cannot maliciously bind to it.  If an idle service's manifest
+     * declaration does not require that permission, it will never be invoked.
+     * </p>
+     */
+    @SdkConstant(SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_INTERFACE =
+            "android.service.idle.IdleService";
+
+    /**
+     * Idle services must be protected with this permission:
+     *
+     * <pre class="prettyprint">
+     *     <service android:name="MyIdleService"
+     *              android:permission="android.permission.BIND_IDLE_SERVICE" >
+     *         ...
+     *     </service>
+     * </pre>
+     *
+     * <p>If an idle service is declared in the manifest but not protected with this
+     * permission, that service will be ignored by the OS.
+     */
+    public static final String PERMISSION_BIND =
+            "android.permission.BIND_IDLE_SERVICE";
+
+    // Trampoline: the callbacks are always run on the main thread
+    IIdleService mBinder = new IIdleService.Stub() {
+        @Override
+        public void startIdleMaintenance(IIdleCallback callbackBinder, int token)
+                throws RemoteException {
+            ensureHandler();
+            Message msg = mHandler.obtainMessage(MSG_START, token, 0, callbackBinder);
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void stopIdleMaintenance(IIdleCallback callbackBinder, int token)
+                throws RemoteException {
+            ensureHandler();
+            Message msg = mHandler.obtainMessage(MSG_STOP, token, 0, callbackBinder);
+            mHandler.sendMessage(msg);
+        }
+    };
+
+    /**
+     * Your application may begin doing "idle" maintenance work in the background.
+     * <p>
+     * Your application may continue to run in the background until it receives a call
+     * to {@link #onIdleStop()}, at which point you <i>must</i> cease doing work.  The
+     * OS will hold a wakelock on your application's behalf from the time this method is
+     * called until after the following call to {@link #onIdleStop()} returns.
+     * </p>
+     * <p>
+     * Returning {@code false} from this method indicates that you have no ongoing work
+     * to do at present.  The OS will respond by immediately calling {@link #onIdleStop()}
+     * and returning your application to its normal stopped state.  Returning {@code true}
+     * indicates that the application is indeed performing ongoing work, so the OS will
+     * let your application run in this state until it's no longer appropriate.
+     * </p>
+     * <p>
+     * You will always receive a matching call to {@link #onIdleStop()} even if your
+     * application returns {@code false} from this method.
+     *
+     * @return {@code true} to indicate that the application wishes to perform some ongoing
+     *     background work; {@code false} to indicate that it does not need to perform such
+     *     work at present.
+     */
+    public abstract boolean onIdleStart();
+
+    /**
+     * Your app's maintenance opportunity is over.  Once the application returns from
+     * this method, the wakelock held by the OS on its behalf will be released.
+     */
+    public abstract void onIdleStop();
+
+    /**
+     * Tell the OS that you have finished your idle work.  Calling this more than once,
+     * or calling it when you have not received an {@link #onIdleStart()} callback, is
+     * an error.
+     *
+     * <p>It is safe to call {@link #finishIdle()} from any thread.
+     */
+    public final void finishIdle() {
+        ensureHandler();
+        mHandler.sendEmptyMessage(MSG_FINISH);
+    }
+
+    class IdleHandler extends Handler {
+        IdleHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_START: {
+                    // Call the concrete onIdleStart(), reporting its return value back to
+                    // the OS.  If onIdleStart() throws, report it as a 'false' return but
+                    // rethrow the exception at the offending app.
+                    boolean result = false;
+                    IIdleCallback callbackBinder = (IIdleCallback) msg.obj;
+                    mCallbackBinder = callbackBinder;
+                    final int token = mToken = msg.arg1;
+                    try {
+                        result = IdleService.this.onIdleStart();
+                    } catch (Exception e) {
+                        Log.e(TAG, "Unable to start idle workload", e);
+                        throw new RuntimeException(e);
+                    } finally {
+                        // don't bother if the service already called finishIdle()
+                        if (mCallbackBinder != null) {
+                            try {
+                                callbackBinder.acknowledgeStart(token, result);
+                            } catch (RemoteException re) {
+                                Log.e(TAG, "System unreachable to start idle workload");
+                            }
+                        }
+                    }
+                    break;
+                }
+
+                case MSG_STOP: {
+                    // Structured just like MSG_START for the stop-idle bookend call.
+                    IIdleCallback callbackBinder = (IIdleCallback) msg.obj;
+                    final int token = msg.arg1;
+                    try {
+                        IdleService.this.onIdleStop();
+                    } catch (Exception e) {
+                        Log.e(TAG, "Unable to stop idle workload", e);
+                        throw new RuntimeException(e);
+                    } finally {
+                        if (mCallbackBinder != null) {
+                            try {
+                                callbackBinder.acknowledgeStop(token);
+                            } catch (RemoteException re) {
+                                Log.e(TAG, "System unreachable to stop idle workload");
+                            }
+                        }
+                    }
+                    break;
+                }
+
+                case MSG_FINISH: {
+                    if (mCallbackBinder != null) {
+                        try {
+                            mCallbackBinder.idleFinished(mToken);
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "System unreachable to finish idling");
+                        } finally {
+                            mCallbackBinder = null;
+                        }
+                    } else {
+                        Log.e(TAG, "finishIdle() called but the idle service is not started");
+                    }
+                    break;
+                }
+
+                default: {
+                    Slog.w(TAG, "Unknown message " + msg.what);
+                }
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public final IBinder onBind(Intent intent) {
+        return mBinder.asBinder();
+    }
+
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b7a06b5..315b119 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1727,6 +1727,13 @@
         android:label="@string/permlab_recovery"
         android:description="@string/permdesc_recovery" />
 
+    <!-- Allows the system to bind to an application's idle services
+         @hide -->
+    <permission android:name="android.permission.BIND_IDLE_SERVICE"
+        android:protectionLevel="signature"
+        android:label="@string/permlab_bindIdleService"
+        android:description="@string/permdesc_bindIdleService" />
+
     <!-- ========================================= -->
     <!-- Permissions for special development tools -->
     <!-- ========================================= -->
@@ -2728,6 +2735,14 @@
             </intent-filter>
         </service>
 
+        <service android:name="com.android.server.MountServiceIdler"
+                 android:exported="false"
+                 android:permission="android.permission.BIND_IDLE_SERVICE" >
+            <intent-filter>
+                <action android:name="android.service.idle.IdleService" />
+            </intent-filter>
+        </service>
+
     </application>
 
 </manifest>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 785e788..ee68199 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1176,6 +1176,11 @@
     <string name="permdesc_manageCaCertificates">Allows the app to install and uninstall CA certificates as trusted credentials.</string>
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_bindIdleService">bind to idle services</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_bindIdleService">Allows the app to interact with application-defined idle services.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_diagnostic">read/write to resources owned by diag</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_diagnostic">Allows the app to read and write to
diff --git a/services/core/java/com/android/server/IdleMaintenanceService.java b/services/core/java/com/android/server/IdleMaintenanceService.java
index b0a1aca..acc6abe 100644
--- a/services/core/java/com/android/server/IdleMaintenanceService.java
+++ b/services/core/java/com/android/server/IdleMaintenanceService.java
@@ -16,22 +16,38 @@
 
 package com.android.server;
 
-import android.app.Activity;
-import android.app.ActivityManagerNative;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
+import android.app.maintenance.IIdleCallback;
+import android.app.maintenance.IIdleService;
+import android.app.maintenance.IdleService;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.WorkSource;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
 
 /**
  * This service observes the device state and when applicable sends
@@ -47,12 +63,15 @@
  *
  * The end of a maintenance window is announced only if: a start was
  * announced AND the screen turned on or a dream was stopped.
+ *
+ * Method naming note:
+ * Methods whose name ends with "Tm" must only be called from the main thread.
  */
 public class IdleMaintenanceService extends BroadcastReceiver {
 
     private static final boolean DEBUG = false;
 
-    private static final String LOG_TAG = IdleMaintenanceService.class.getSimpleName();
+    private static final String TAG = IdleMaintenanceService.class.getSimpleName();
 
     private static final int LAST_USER_ACTIVITY_TIME_INVALID = -1;
 
@@ -74,36 +93,480 @@
     private static final String ACTION_FORCE_IDLE_MAINTENANCE =
         "com.android.server.IdleMaintenanceService.action.FORCE_IDLE_MAINTENANCE";
 
-    private static final Intent sIdleMaintenanceStartIntent;
-    static {
-        sIdleMaintenanceStartIntent = new Intent(Intent.ACTION_IDLE_MAINTENANCE_START);
-        sIdleMaintenanceStartIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-    };
+    static final int MSG_OP_COMPLETE = 1;
+    static final int MSG_IDLE_FINISHED = 2;
+    static final int MSG_TIMEOUT = 3;
 
-    private static final Intent sIdleMaintenanceEndIntent;
-    static {
-        sIdleMaintenanceEndIntent = new Intent(Intent.ACTION_IDLE_MAINTENANCE_END);
-        sIdleMaintenanceEndIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-    }
+    // when a timeout happened, what were we expecting?
+    static final int VERB_BINDING = 1;
+    static final int VERB_IDLING = 2;
+    static final int VERB_ENDING = 3;
+
+    // What are our relevant timeouts / allocated slices?
+    static final long OP_TIMEOUT = 8 * 1000;  // 8 seconds to bind or ack the start
+    static final long IDLE_TIMESLICE = 10 * 60 * 1000;  // ten minutes for each idler
 
     private final AlarmManager mAlarmService;
-
     private final BatteryService mBatteryService;
-
     private final PendingIntent mUpdateIdleMaintenanceStatePendingIntent;
-
     private final Context mContext;
-
     private final WakeLock mWakeLock;
-
-    private final Handler mHandler;
+    private final WorkSource mSystemWorkSource = new WorkSource(Process.myUid());
 
     private long mLastIdleMaintenanceStartTimeMillis;
-
     private long mLastUserActivityElapsedTimeMillis = LAST_USER_ACTIVITY_TIME_INVALID;
-
     private boolean mIdleMaintenanceStarted;
 
+    final IdleCallback mCallback;
+    final Handler mHandler;
+
+    final Random mTokenGenerator = new Random();
+
+    int makeToken() {
+        int token;
+        do  {
+            token = mTokenGenerator.nextInt(Integer.MAX_VALUE);
+        } while (token == 0);
+        return token;
+    }
+
+    class ActiveTask {
+        public IdleServiceInfo who;
+        public int verb;
+        public int token;
+
+        ActiveTask(IdleServiceInfo target, int action) {
+            who = target;
+            verb = action;
+            token = makeToken();
+        }
+
+        @Override
+        public String toString() {
+            return "ActiveTask{" + Integer.toHexString(this.hashCode())
+                    + " : verb=" + verb
+                    + " : token=" + token
+                    + " : "+ who + "}";
+        }
+    }
+
+    // What operations are in flight?
+    final SparseArray<ActiveTask> mPendingOperations = new SparseArray<ActiveTask>();
+
+    // Idle service queue management
+    class IdleServiceInfo {
+        public final ComponentName componentName;
+        public final int uid;
+        public IIdleService service;
+
+        IdleServiceInfo(ResolveInfo info, ComponentName cname) {
+            componentName = cname;  // derived from 'info' but this avoids an extra object
+            uid = info.serviceInfo.applicationInfo.uid;
+            service = null;
+        }
+
+        @Override
+        public int hashCode() {
+            return componentName.hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return "IdleServiceInfo{" + componentName
+                    + " / " + (service == null ? "null" : service.asBinder()) + "}";
+        }
+    }
+
+    final ArrayMap<ComponentName, IdleServiceInfo> mIdleServices =
+            new ArrayMap<ComponentName, IdleServiceInfo>();
+    final LinkedList<IdleServiceInfo> mIdleServiceQueue = new LinkedList<IdleServiceInfo>();
+    IdleServiceInfo mCurrentIdler;  // set when we've committed to launching an idler
+    IdleServiceInfo mLastIdler;     // end of queue when idling begins
+
+    void reportNoTimeout(int token, boolean result) {
+        final Message msg = mHandler.obtainMessage(MSG_OP_COMPLETE, result ? 1 : 0, token);
+        mHandler.sendMessage(msg);
+    }
+
+    // Binder acknowledgment trampoline
+    class IdleCallback extends IIdleCallback.Stub {
+        @Override
+        public void acknowledgeStart(int token, boolean result) throws RemoteException {
+            reportNoTimeout(token, result);
+        }
+
+        @Override
+        public void acknowledgeStop(int token) throws RemoteException {
+            reportNoTimeout(token, false);
+        }
+
+        @Override
+        public void idleFinished(int token) throws RemoteException {
+            if (DEBUG) {
+                Slog.v(TAG, "idleFinished: " + token);
+            }
+            final Message msg = mHandler.obtainMessage(MSG_IDLE_FINISHED, 0, token);
+            mHandler.sendMessage(msg);
+        }
+    }
+
+    // Stuff that we run on a Handler
+    class IdleHandler extends Handler {
+        public IdleHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            final int token = msg.arg2;
+
+            switch (msg.what) {
+                case MSG_OP_COMPLETE: {
+                    if (DEBUG) {
+                        Slog.i(TAG, "MSG_OP_COMPLETE of " + token);
+                    }
+                    ActiveTask task = mPendingOperations.get(token);
+                    if (task != null) {
+                        mPendingOperations.remove(token);
+                        removeMessages(MSG_TIMEOUT);
+
+                        handleOpCompleteTm(task, msg.arg1);
+                    } else {
+                        // Can happen in a race between timeout and actual
+                        // (belated) completion of a "begin idling" or similar
+                        // operation.  In that state we've already processed the
+                        // timeout, so we intentionally no-op here.
+                        if (DEBUG) {
+                            Slog.w(TAG, "Belated op-complete of " + token);
+                        }
+                    }
+                    break;
+                }
+
+                case MSG_IDLE_FINISHED: {
+                    if (DEBUG) {
+                        Slog.i(TAG, "MSG_IDLE_FINISHED of " + token);
+                    }
+                    ActiveTask task = mPendingOperations.get(token);
+                    if (task != null) {
+                        if (DEBUG) {
+                            Slog.i(TAG, "... removing task " + token);
+                        }
+                        mPendingOperations.remove(token);
+                        removeMessages(MSG_TIMEOUT);
+
+                        handleIdleFinishedTm(task);
+                    } else {
+                        // Can happen "legitimately" from an app explicitly calling
+                        // idleFinished() after already having been told that its slice
+                        // has ended.
+                        if (DEBUG) {
+                            Slog.w(TAG, "Belated idle-finished of " + token);
+                        }
+                    }
+                    break;
+                }
+
+                case MSG_TIMEOUT: {
+                    if (DEBUG) {
+                        Slog.i(TAG, "MSG_TIMEOUT of " + token);
+                    }
+                    ActiveTask task = mPendingOperations.get(token);
+                    if (task != null) {
+                        mPendingOperations.remove(token);
+                        removeMessages(MSG_OP_COMPLETE);
+
+                        handleTimeoutTm(task);
+                    } else {
+                        // This one should not happen; we flushed timeout messages
+                        // whenever we entered a state after which we have established
+                        // that they are not appropriate.
+                        Slog.w(TAG, "Unexpected timeout of " + token);
+                    }
+                    break;
+                }
+
+                default:
+                    Slog.w(TAG, "Unknown message: " + msg.what);
+            }
+        }
+    }
+
+    void handleTimeoutTm(ActiveTask task) {
+        switch (task.verb) {
+        case VERB_BINDING: {
+            // We were trying to bind to this service, but it wedged or otherwise
+            // failed to respond in time.  Let it stay in the queue for the next
+            // time around, but just give up on it for now and go on to the next.
+            startNextIdleServiceTm();
+            break;
+        }
+        case VERB_IDLING: {
+            // The service has reached the end of its designated idle timeslice.
+            // This is not considered an error.
+            if (DEBUG) {
+                Slog.i(TAG, "Idler reached end of timeslice: " + task.who);
+            }
+            sendEndIdleTm(task.who);
+            break;
+        }
+        case VERB_ENDING: {
+            if (mCurrentIdler == task.who) {
+                if (DEBUG) {
+                    Slog.i(TAG, "Task timed out when ending; unbind needed");
+                }
+                handleIdleFinishedTm(task);
+            } else {
+                if (DEBUG) {
+                    Slog.w(TAG, "Ending timeout for non-current idle service!");
+                }
+            }
+            break;
+        }
+        default: {
+            Slog.w(TAG, "Unknown timeout state " + task.verb);
+            break;
+        }
+        }
+    }
+
+    void handleOpCompleteTm(ActiveTask task, int result) {
+        if (DEBUG) {
+            Slog.i(TAG, "handleOpComplete : task=" + task + " result=" + result);
+        }
+        if (task.verb == VERB_IDLING) {
+            // If the service was told to begin idling and responded positively, then
+            // it has begun idling and will eventually either explicitly finish, or
+            // reach the end of its allotted timeslice.  It's running free now, so we
+            // just schedule the idle-expiration timeout under the token it's already been
+            // given and let it keep going.
+            if (result != 0) {
+                scheduleOpTimeoutTm(task);
+            } else {
+                // The idle service has indicated that it does not, in fact,
+                // need to run at present, so we immediately indicate that it's
+                // to finish idling, and go on to the next idler.
+                if (DEBUG) {
+                    Slog.i(TAG, "Idler declined idling; moving along");
+                }
+                sendEndIdleTm(task.who);
+            }
+        } else {
+            // In the idling case, the task will be cleared either as the result of a timeout
+            // or of an explicit idleFinished().  For all other operations (binding, ending) we
+            // are done with the task as such, so we remove it from our bookkeeping.
+            if (DEBUG) {
+                Slog.i(TAG, "Clearing task " + task);
+            }
+            mPendingOperations.remove(task.token);
+            if (task.verb == VERB_ENDING) {
+                // The last bit of handshaking around idle cessation for this target
+                handleIdleFinishedTm(task);
+            }
+        }
+    }
+
+    void handleIdleFinishedTm(ActiveTask task) {
+        final IdleServiceInfo who = task.who;
+        if (who == mCurrentIdler) {
+            if (DEBUG) {
+                Slog.i(TAG, "Current idler has finished: " + who);
+                Slog.i(TAG, "Attributing wakelock to system work source");
+            }
+            mContext.unbindService(mConnection);
+            startNextIdleServiceTm();
+        } else {
+            Slog.w(TAG, "finish from non-current idle service? " + who);
+        }
+    }
+
+    void updateIdleServiceQueueTm() {
+        if (DEBUG) {
+            Slog.i(TAG, "Updating idle service queue");
+        }
+        PackageManager pm = mContext.getPackageManager();
+        Intent idleIntent = new Intent(IdleService.SERVICE_INTERFACE);
+        List<ResolveInfo> services = pm.queryIntentServices(idleIntent, 0);
+        for (ResolveInfo info : services) {
+            if (info.serviceInfo != null) {
+                if (IdleService.PERMISSION_BIND.equals(info.serviceInfo.permission)) {
+                    final ComponentName componentName = new ComponentName(
+                            info.serviceInfo.packageName,
+                            info.serviceInfo.name);
+                    if (DEBUG) {
+                        Slog.i(TAG, "   - " + componentName);
+                    }
+                    if (!mIdleServices.containsKey(componentName)) {
+                        if (DEBUG) {
+                            Slog.i(TAG, "      + not known; adding");
+                        }
+                        IdleServiceInfo serviceInfo = new IdleServiceInfo(info, componentName);
+                        mIdleServices.put(componentName, serviceInfo);
+                        mIdleServiceQueue.add(serviceInfo);
+                    }
+                } else {
+                    if (DEBUG) {
+                        Slog.i(TAG, "Idle service " + info.serviceInfo
+                                + " does not have required permission; ignoring");
+                    }
+                }
+            }
+        }
+    }
+
+    void startNextIdleServiceTm() {
+        mWakeLock.setWorkSource(mSystemWorkSource);
+
+        if (mLastIdler == null) {
+            // we've run the queue; nothing more to do until the next idle interval.
+            if (DEBUG) {
+                Slog.i(TAG, "Queue already drained; nothing more to do");
+            }
+            return;
+        }
+
+        if (DEBUG) {
+            Slog.i(TAG, "startNextIdleService : last=" + mLastIdler + " cur=" + mCurrentIdler);
+            if (mIdleServiceQueue.size() > 0) {
+                int i = 0;
+                Slog.i(TAG, "Queue (" + mIdleServiceQueue.size() + "):");
+                for (IdleServiceInfo info : mIdleServiceQueue) {
+                    Slog.i(TAG, "   " + i + " : " + info);
+                    i++;
+                }
+            }
+        }
+        if (mCurrentIdler != mLastIdler) {
+            if (mIdleServiceQueue.size() > 0) {
+                IdleServiceInfo target = mIdleServiceQueue.pop();
+                if (DEBUG) {
+                    Slog.i(TAG, "starting next idle service " + target);
+                }
+                Intent idleIntent = new Intent(IdleService.SERVICE_INTERFACE);
+                idleIntent.setComponent(target.componentName);
+                mCurrentIdler = target;
+                ActiveTask task = new ActiveTask(target, VERB_BINDING);
+                scheduleOpTimeoutTm(task);
+                boolean bindOk = mContext.bindServiceAsUser(idleIntent, mConnection,
+                        Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY, UserHandle.OWNER);
+                if (!bindOk) {
+                    if (DEBUG) {
+                        Slog.w(TAG, "bindService() to " + target.componentName
+                                + " failed");
+                    }
+                } else {
+                    mIdleServiceQueue.add(target);  // at the end for next time
+                    if (DEBUG) { Slog.i(TAG, "Attributing wakelock to target uid " + target.uid); }
+                    mWakeLock.setWorkSource(new WorkSource(target.uid));
+                }
+            } else {
+                // Queue is empty but mLastIdler is non-null -- eeep.  Clear *everything*
+                // and wind up until the next time around.
+                Slog.e(TAG, "Queue unexpectedly empty; resetting.  last="
+                        + mLastIdler + " cur=" + mCurrentIdler);
+                mHandler.removeMessages(MSG_TIMEOUT);
+                mPendingOperations.clear();
+                stopIdleMaintenanceTm();
+            }
+        } else {
+            // we've reached the place we started, so mark the queue as drained
+            if (DEBUG) {
+                Slog.i(TAG, "Reached end of queue.");
+            }
+            stopIdleMaintenanceTm();
+        }
+    }
+
+    void sendStartIdleTm(IdleServiceInfo who) {
+        ActiveTask task = new ActiveTask(who, VERB_IDLING);
+        scheduleOpTimeoutTm(task);
+        try {
+            who.service.startIdleMaintenance(mCallback, task.token);
+        } catch (RemoteException e) {
+            // We bound to it, but now we can't reach it.  Bail and go on to the
+            // next service.
+            mContext.unbindService(mConnection);
+            if (DEBUG) { Slog.i(TAG, "Attributing wakelock to system work source"); }
+            mHandler.removeMessages(MSG_TIMEOUT);
+            startNextIdleServiceTm();
+        }
+    }
+
+    void sendEndIdleTm(IdleServiceInfo who) {
+        ActiveTask task = new ActiveTask(who, VERB_ENDING);
+        scheduleOpTimeoutTm(task);
+        if (DEBUG) {
+            Slog.i(TAG, "Sending end-idle to " + who);
+        }
+        try {
+            who.service.stopIdleMaintenance(mCallback, task.token);
+        } catch (RemoteException e) {
+            // We bound to it, but now we can't reach it.  Bail and go on to the
+            // next service.
+            mContext.unbindService(mConnection);
+            if (DEBUG) { Slog.i(TAG, "Attributing wakelock to system work source"); }
+            mHandler.removeMessages(MSG_TIMEOUT);
+            startNextIdleServiceTm();
+        }
+    }
+
+    ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            if (DEBUG) {
+                Slog.i(TAG, "onServiceConnected(" + name + ")");
+            }
+            IdleServiceInfo info = mIdleServices.get(name);
+            if (info != null) {
+                // Bound!  Cancel the bind timeout
+                mHandler.removeMessages(MSG_TIMEOUT);
+                // Now tell it to start its idle work
+                info.service = IIdleService.Stub.asInterface(service);
+                sendStartIdleTm(info);
+            } else {
+                // We bound to a service we don't know about.  That's ungood.
+                Slog.e(TAG, "Connected to unexpected component " + name);
+                mContext.unbindService(this);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (DEBUG) {
+                Slog.i(TAG, "onServiceDisconnected(" + name + ")");
+            }
+            IdleServiceInfo who = mIdleServices.get(name);
+            if (who == mCurrentIdler) {
+                // Hm, okay; they didn't tell us they were finished but they
+                // went away.  Crashed, probably.  Oh well.  They're gone, so
+                // we can't finish them cleanly; just force things along.
+                Slog.w(TAG, "Idler unexpectedly vanished: " + mCurrentIdler);
+                mContext.unbindService(this);
+                mHandler.removeMessages(MSG_TIMEOUT);
+                startNextIdleServiceTm();
+            } else {
+                // Not the current idler, so we don't interrupt our process...
+                if (DEBUG) {
+                    Slog.w(TAG, "Disconnect of abandoned or unexpected service " + name);
+                }
+            }
+        }
+    };
+
+    // Schedules a timeout / end-of-work based on the task verb
+    void scheduleOpTimeoutTm(ActiveTask task) {
+        final long timeoutMillis = (task.verb == VERB_IDLING) ? IDLE_TIMESLICE : OP_TIMEOUT;
+        if (DEBUG) {
+            Slog.i(TAG, "Scheduling timeout (token " + task.token
+                    + " : verb " + task.verb + ") for " + task + " in " + timeoutMillis);
+        }
+        mPendingOperations.put(task.token, task);
+        mHandler.removeMessages(MSG_TIMEOUT);
+        final Message msg = mHandler.obtainMessage(MSG_TIMEOUT, 0, task.token);
+        mHandler.sendMessageDelayed(msg, timeoutMillis);
+    }
+
+    // -------------------------------------------------------------------------------
     public IdleMaintenanceService(Context context, BatteryService batteryService) {
         mContext = context;
         mBatteryService = batteryService;
@@ -111,9 +574,10 @@
         mAlarmService = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 
         PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
 
-        mHandler = new Handler(mContext.getMainLooper());
+        mHandler = new IdleHandler(mContext.getMainLooper());
+        mCallback = new IdleCallback();
 
         Intent intent = new Intent(ACTION_UPDATE_IDLE_MAINTENANCE_STATE);
         intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -159,26 +623,28 @@
         mAlarmService.cancel(mUpdateIdleMaintenanceStatePendingIntent);
     }
 
-    private void updateIdleMaintenanceState(boolean noisy) {
+    private void updateIdleMaintenanceStateTm(boolean noisy) {
         if (mIdleMaintenanceStarted) {
             // Idle maintenance can be interrupted by user activity, or duration
             // time out, or low battery.
-            if (!lastUserActivityPermitsIdleMaintenanceRunning()
-                    || !batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning()) {
+            final boolean batteryOk
+                    = batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning();
+            if (!lastUserActivityPermitsIdleMaintenanceRunning() || !batteryOk) {
                 unscheduleUpdateIdleMaintenanceState();
                 mIdleMaintenanceStarted = false;
-                EventLogTags.writeIdleMaintenanceWindowFinish(SystemClock.elapsedRealtime(),
-                        mLastUserActivityElapsedTimeMillis, mBatteryService.getBatteryLevel(),
-                        isBatteryCharging() ? 1 : 0);
-                sendIdleMaintenanceEndIntent();
                 // We stopped since we don't have enough battery or timed out but the
                 // user is not using the device, so we should be able to run maintenance
                 // in the next maintenance window since the battery may be charged
                 // without interaction and the min interval between maintenances passed.
-                if (!batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning()) {
+                if (!batteryOk) {
                     scheduleUpdateIdleMaintenanceState(
                             getNextIdleMaintenanceIntervalStartFromNow());
                 }
+
+                EventLogTags.writeIdleMaintenanceWindowFinish(SystemClock.elapsedRealtime(),
+                        mLastUserActivityElapsedTimeMillis, mBatteryService.getBatteryLevel(),
+                        isBatteryCharging() ? 1 : 0);
+                scheduleIdleFinishTm();
             }
         } else if (deviceStatePermitsIdleMaintenanceStart(noisy)
                 && lastUserActivityPermitsIdleMaintenanceStart(noisy)
@@ -191,7 +657,7 @@
                     mLastUserActivityElapsedTimeMillis, mBatteryService.getBatteryLevel(),
                     isBatteryCharging() ? 1 : 0);
             mLastIdleMaintenanceStartTimeMillis = SystemClock.elapsedRealtime();
-            sendIdleMaintenanceStartIntent();
+            startIdleMaintenanceTm();
         } else if (lastUserActivityPermitsIdleMaintenanceStart(noisy)) {
              if (lastRunPermitsIdleMaintenanceStart(noisy)) {
                 // The user does not use the device and we did not run maintenance in more
@@ -207,27 +673,57 @@
         }
     }
 
+    void startIdleMaintenanceTm() {
+        if (DEBUG) {
+            Slog.i(TAG, "*** Starting idle maintenance ***");
+        }
+        if (DEBUG) { Slog.i(TAG, "Attributing wakelock to system work source"); }
+        mWakeLock.setWorkSource(mSystemWorkSource);
+        mWakeLock.acquire();
+        updateIdleServiceQueueTm();
+        mCurrentIdler = null;
+        mLastIdler = (mIdleServiceQueue.size() > 0) ? mIdleServiceQueue.peekLast() : null;
+        startNextIdleServiceTm();
+    }
+
+    // Start a graceful wind-down of the idle maintenance state: end the current idler
+    // and pretend that we've finished running the queue.  If there's no current idler,
+    // this is a no-op.
+    void scheduleIdleFinishTm() {
+        if (mCurrentIdler != null) {
+            if (DEBUG) {
+                Slog.i(TAG, "*** Finishing idle maintenance ***");
+            }
+            mLastIdler = mCurrentIdler;
+            sendEndIdleTm(mCurrentIdler);
+        } else {
+            if (DEBUG) {
+                Slog.w(TAG, "Asked to finish idle maintenance but we're done already");
+            }
+        }
+    }
+
+    // Actual finalization of the idle maintenance sequence
+    void stopIdleMaintenanceTm() {
+        if (mLastIdler != null) {
+            if (DEBUG) {
+                Slog.i(TAG, "*** Idle maintenance shutdown ***");
+            }
+            mWakeLock.setWorkSource(mSystemWorkSource);
+            mLastIdler = mCurrentIdler = null;
+            updateIdleMaintenanceStateTm(false);   // resets 'started' and schedules next window
+            mWakeLock.release();
+        } else {
+            Slog.e(TAG, "ERROR: idle shutdown but invariants not held.  last=" + mLastIdler
+                    + " cur=" + mCurrentIdler + " size=" + mIdleServiceQueue.size());
+        }
+    }
+
     private long getNextIdleMaintenanceIntervalStartFromNow() {
         return mLastIdleMaintenanceStartTimeMillis + MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS
                 - SystemClock.elapsedRealtime();
     }
 
-    private void sendIdleMaintenanceStartIntent() {
-        mWakeLock.acquire();
-        try {
-            ActivityManagerNative.getDefault().performIdleMaintenance();
-        } catch (RemoteException e) {
-        }
-        mContext.sendOrderedBroadcastAsUser(sIdleMaintenanceStartIntent, UserHandle.ALL,
-                null, this, mHandler, Activity.RESULT_OK, null, null);
-    }
-
-    private void sendIdleMaintenanceEndIntent() {
-        mWakeLock.acquire();
-        mContext.sendOrderedBroadcastAsUser(sIdleMaintenanceEndIntent, UserHandle.ALL,
-                null, this, mHandler, Activity.RESULT_OK, null, null);
-    }
-
     private boolean deviceStatePermitsIdleMaintenanceStart(boolean noisy) {
         final int minBatteryLevel = isBatteryCharging()
                 ? MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_CHARGING
@@ -281,7 +777,7 @@
     @Override
     public void onReceive(Context context, Intent intent) {
         if (DEBUG) {
-            Log.i(LOG_TAG, intent.getAction());
+            Log.i(TAG, intent.getAction());
         }
         String action = intent.getAction();
         if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
@@ -292,7 +788,7 @@
             // next release. The only client for this for now is internal an holds
             // a wake lock correctly.
             if (mIdleMaintenanceStarted) {
-                updateIdleMaintenanceState(false);
+                updateIdleMaintenanceStateTm(false);
             }
         } else if (Intent.ACTION_SCREEN_ON.equals(action)
                 || Intent.ACTION_DREAMING_STOPPED.equals(action)) {
@@ -302,7 +798,7 @@
             unscheduleUpdateIdleMaintenanceState();
             // If the screen went on/stopped dreaming, we know the user is using the
             // device which means that idle maintenance should be stopped if running.
-            updateIdleMaintenanceState(false);
+            updateIdleMaintenanceStateTm(false);
         } else if (Intent.ACTION_SCREEN_OFF.equals(action)
                 || Intent.ACTION_DREAMING_STARTED.equals(action)) {
             mLastUserActivityElapsedTimeMillis = SystemClock.elapsedRealtime();
@@ -311,17 +807,12 @@
             // this timeout elapses since the device may go to sleep by then.
             scheduleUpdateIdleMaintenanceState(MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START);
         } else if (ACTION_UPDATE_IDLE_MAINTENANCE_STATE.equals(action)) {
-            updateIdleMaintenanceState(false);
+            updateIdleMaintenanceStateTm(false);
         } else if (ACTION_FORCE_IDLE_MAINTENANCE.equals(action)) {
             long now = SystemClock.elapsedRealtime() - 1;
             mLastUserActivityElapsedTimeMillis = now - MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START;
             mLastIdleMaintenanceStartTimeMillis = now - MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS;
-            updateIdleMaintenanceState(true);
-        } else if (Intent.ACTION_IDLE_MAINTENANCE_START.equals(action)
-                || Intent.ACTION_IDLE_MAINTENANCE_END.equals(action)) {
-            // We were holding a wake lock while broadcasting the idle maintenance
-            // intents but now that we finished the broadcast release the wake lock.
-            mWakeLock.release();
+            updateIdleMaintenanceStateTm(true);
         }
     }
 }
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index 816ae69..e6e4bca 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -107,6 +107,9 @@
 class MountService extends IMountService.Stub
         implements INativeDaemonConnectorCallbacks, Watchdog.Monitor {
 
+    // Static direct instance pointer for the tightly-coupled idle service to use
+    static MountService sSelf = null;
+
     // TODO: listen for user creation/deletion
 
     private static final boolean LOCAL_LOGD = false;
@@ -345,6 +348,7 @@
     private static final int H_UNMOUNT_PM_DONE = 2;
     private static final int H_UNMOUNT_MS = 3;
     private static final int H_SYSTEM_READY = 4;
+    private static final int H_FSTRIM = 5;
 
     private static final int RETRY_UNMOUNT_DELAY = 30; // in ms
     private static final int MAX_UNMOUNT_RETRIES = 4;
@@ -494,6 +498,24 @@
                     }
                     break;
                 }
+                case H_FSTRIM: {
+                    waitForReady();
+                    Slog.i(TAG, "Running fstrim idle maintenance");
+                    try {
+                        // This method must be run on the main (handler) thread,
+                        // so it is safe to directly call into vold.
+                        mConnector.execute("fstrim", "dotrim");
+                        EventLogTags.writeFstrimStart(SystemClock.elapsedRealtime());
+                    } catch (NativeDaemonConnectorException ndce) {
+                        Slog.e(TAG, "Failed to run fstrim!");
+                    }
+                    // invoke the completion callback, if any
+                    Runnable callback = (Runnable) msg.obj;
+                    if (callback != null) {
+                        callback.run();
+                    }
+                    break;
+                }
             }
         }
     };
@@ -608,27 +630,6 @@
         }
     };
 
-    private final BroadcastReceiver mIdleMaintenanceReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            waitForReady();
-            String action = intent.getAction();
-            // Since fstrim will be run on a daily basis we do not expect
-            // fstrim to be too long, so it is not interruptible. We will
-            // implement interruption only in case we see issues.
-            if (Intent.ACTION_IDLE_MAINTENANCE_START.equals(action)) {
-                try {
-                    // This method runs on the handler thread,
-                    // so it is safe to directly call into vold.
-                    mConnector.execute("fstrim", "dotrim");
-                    EventLogTags.writeFstrimStart(SystemClock.elapsedRealtime());
-                } catch (NativeDaemonConnectorException ndce) {
-                    Slog.e(TAG, "Failed to run fstrim!");
-                }
-            }
-        }
-    };
-
     private final class MountServiceBinderListener implements IBinder.DeathRecipient {
         final IMountServiceListener mListener;
 
@@ -646,6 +647,10 @@
         }
     }
 
+    void runIdleMaintenance(Runnable callback) {
+        mHandler.sendMessage(mHandler.obtainMessage(H_FSTRIM, callback));
+    }
+
     private void doShareUnshareVolume(String path, String method, boolean enable) {
         // TODO: Add support for multiple share methods
         if (!method.equals("ums")) {
@@ -1337,6 +1342,8 @@
      * @param context  Binder context for this service
      */
     public MountService(Context context) {
+        sSelf = this;
+
         mContext = context;
 
         synchronized (mVolumesLock) {
@@ -1363,12 +1370,6 @@
                     mUsbReceiver, new IntentFilter(UsbManager.ACTION_USB_STATE), null, mHandler);
         }
 
-        // Watch for idle maintenance changes
-        IntentFilter idleMaintenanceFilter = new IntentFilter();
-        idleMaintenanceFilter.addAction(Intent.ACTION_IDLE_MAINTENANCE_START);
-        mContext.registerReceiverAsUser(mIdleMaintenanceReceiver, UserHandle.ALL,
-                idleMaintenanceFilter, null, mHandler);
-
         // Add OBB Action Handler to MountService thread.
         mObbActionHandler = new ObbActionHandler(IoThread.get().getLooper());
 
diff --git a/services/core/java/com/android/server/MountServiceIdler.java b/services/core/java/com/android/server/MountServiceIdler.java
new file mode 100644
index 0000000..8b19321
--- /dev/null
+++ b/services/core/java/com/android/server/MountServiceIdler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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;
+
+import android.app.maintenance.IdleService;
+import android.util.Slog;
+
+public class MountServiceIdler extends IdleService {
+    private static final String TAG = "MountServiceIdler";
+
+    private Runnable mFinishCallback = new Runnable() {
+        @Override
+        public void run() {
+            Slog.i(TAG, "Got mount service completion callback");
+            finishIdle();
+        }
+    };
+
+    @Override
+    public boolean onIdleStart() {
+        // The mount service will run an fstrim operation asynchronously
+        // on a designated separate thread, so we provide it with a callback
+        // that lets us cleanly end our idle timeslice.  It's safe to call
+        // finishIdle() from any thread.
+        MountService ms = MountService.sSelf;
+        if (ms != null) {
+            ms.runIdleMaintenance(mFinishCallback);
+        }
+        return ms != null;
+    }
+
+    @Override
+    public void onIdleStop() {
+    }
+}
diff --git a/tests/IdleServiceTest/Android.mk b/tests/IdleServiceTest/Android.mk
new file mode 100644
index 0000000..a7879c5
--- /dev/null
+++ b/tests/IdleServiceTest/Android.mk
@@ -0,0 +1,13 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := IdleServiceTest
+LOCAL_CERTIFICATE := platform
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
diff --git a/tests/IdleServiceTest/AndroidManifest.xml b/tests/IdleServiceTest/AndroidManifest.xml
new file mode 100644
index 0000000..16d2324
--- /dev/null
+++ b/tests/IdleServiceTest/AndroidManifest.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.idleservicetest">
+
+    <application>
+        <service android:name="TestService"
+                 android:exported="true"
+                 android:enabled="true"
+                 android:permission="android.permission.BIND_IDLE_SERVICE" >
+            <intent-filter>
+                <action android:name="android.service.idle.IdleService" />
+            </intent-filter>
+        </service>
+
+        <service android:name="CrashingTestService"
+                 android:exported="true"
+                 android:enabled="true"
+                 android:permission="android.permission.BIND_IDLE_SERVICE" >
+            <intent-filter>
+                <action android:name="android.service.idle.IdleService" />
+            </intent-filter>
+        </service>
+
+        <service android:name="TimeoutTestService"
+                 android:exported="true"
+                 android:enabled="true"
+                 android:permission="android.permission.BIND_IDLE_SERVICE" >
+            <intent-filter>
+                <action android:name="android.service.idle.IdleService" />
+            </intent-filter>
+        </service>
+
+        <!-- UnpermissionedTestService should never run because it does
+             not require the necessary permission in its <service> block -->
+        <service android:name="UnpermissionedTestService"
+                 android:exported="true"
+                 android:enabled="true" >
+            <intent-filter>
+                <action android:name="android.service.idle.IdleService" />
+            </intent-filter>
+        </service>
+
+    </application>
+</manifest>
diff --git a/tests/IdleServiceTest/src/com/android/idleservicetest/CrashingTestService.java b/tests/IdleServiceTest/src/com/android/idleservicetest/CrashingTestService.java
new file mode 100644
index 0000000..022ebcf
--- /dev/null
+++ b/tests/IdleServiceTest/src/com/android/idleservicetest/CrashingTestService.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2014 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.idleservicetest;
+
+import android.app.maintenance.IdleService;
+import android.os.Handler;
+import android.util.Log;
+
+public class CrashingTestService extends IdleService {
+    static final String TAG = "CrashingTestService";
+
+    String mNull = null;
+
+    @Override
+    public boolean onIdleStart() {
+        Log.i(TAG, "Idle maintenance: onIdleStart()");
+
+        Handler h = new Handler();
+        Runnable r = new Runnable() {
+            @Override
+            public void run() {
+                Log.i(TAG, "Explicitly crashing");
+                if (mNull.equals("")) {
+                    Log.i(TAG, "won't happen");
+                }
+            }
+        };
+        Log.i(TAG, "Posting explicit crash in 15 seconds");
+        h.postDelayed(r, 15 * 1000);
+        return true;
+    }
+
+    @Override
+    public void onIdleStop() {
+        Log.i(TAG, "Idle maintenance: onIdleStop()");
+    }
+
+}
diff --git a/tests/IdleServiceTest/src/com/android/idleservicetest/TestService.java b/tests/IdleServiceTest/src/com/android/idleservicetest/TestService.java
new file mode 100644
index 0000000..7e9805f
--- /dev/null
+++ b/tests/IdleServiceTest/src/com/android/idleservicetest/TestService.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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.idleservicetest;
+
+import android.app.maintenance.IdleService;
+import android.os.Handler;
+import android.util.Log;
+
+public class TestService extends IdleService {
+    static final String TAG = "TestService";
+
+    @Override
+    public boolean onIdleStart() {
+        Log.i(TAG, "Idle maintenance: onIdleStart()");
+
+        Handler h = new Handler();
+        Runnable r = new Runnable() {
+            @Override
+            public void run() {
+                Log.i(TAG, "Explicitly finishing idle");
+                finishIdle();
+            }
+        };
+        Log.i(TAG, "Posting explicit finish in 15 seconds");
+        h.postDelayed(r, 15 * 1000);
+        return true;
+    }
+
+    @Override
+    public void onIdleStop() {
+        Log.i(TAG, "Idle maintenance: onIdleStop()");
+    }
+
+}
diff --git a/tests/IdleServiceTest/src/com/android/idleservicetest/TimeoutTestService.java b/tests/IdleServiceTest/src/com/android/idleservicetest/TimeoutTestService.java
new file mode 100644
index 0000000..b2ba21b
--- /dev/null
+++ b/tests/IdleServiceTest/src/com/android/idleservicetest/TimeoutTestService.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 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.idleservicetest;
+
+import android.app.maintenance.IdleService;
+import android.util.Log;
+
+public class TimeoutTestService extends IdleService {
+    private static final String TAG = "TimeoutTestService";
+
+    @Override
+    public boolean onIdleStart() {
+        Log.i(TAG, "onIdleStart() but anticipating time-slice timeout");
+        return true;
+    }
+
+    @Override
+    public void onIdleStop() {
+        Log.i(TAG, "onIdleStop() so we're done");
+    }
+
+}
diff --git a/tests/IdleServiceTest/src/com/android/idleservicetest/UnpermissionedTestService.java b/tests/IdleServiceTest/src/com/android/idleservicetest/UnpermissionedTestService.java
new file mode 100644
index 0000000..b9fe32b
--- /dev/null
+++ b/tests/IdleServiceTest/src/com/android/idleservicetest/UnpermissionedTestService.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 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.idleservicetest;
+
+import android.app.maintenance.IdleService;
+import android.util.Log;
+
+// Should never be invoked because its manifest declaration does not
+// require the necessary permission.
+public class UnpermissionedTestService extends IdleService {
+    private static final String TAG = "UnpermissionedTestService";
+
+    @Override
+    public boolean onIdleStart() {
+        Log.e(TAG, "onIdleStart() for this service should never be called!");
+        return false;
+    }
+
+    @Override
+    public void onIdleStop() {
+        Log.e(TAG, "onIdleStop() for this service should never be called!");
+    }
+
+}