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!");
+ }
+
+}