blob: acc6abe89b33ad2155f7bd1840178a7b07b8f209 [file] [log] [blame]
/*
* Copyright (C) 2013 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.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
* broadcasts at the beginning and at the end of a period during which
* observers can perform idle maintenance tasks. Typical use of the
* idle maintenance is to perform somehow expensive tasks that can be
* postponed to a moment when they will not degrade user experience.
*
* The current implementation is very simple. The start of a maintenance
* window is announced if: the screen is off or showing a dream AND the
* battery level is more than twenty percent AND at least one hour passed
* activity).
*
* 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 TAG = IdleMaintenanceService.class.getSimpleName();
private static final int LAST_USER_ACTIVITY_TIME_INVALID = -1;
private static final long MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS = 24 * 60 * 60 * 1000; // 1 day
private static final int MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_CHARGING = 30; // percent
private static final int MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_NOT_CHARGING = 80; // percent
private static final int MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_RUNNING = 20; // percent
private static final long MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START = 71 * 60 * 1000; // 71 min
private static final long MAX_IDLE_MAINTENANCE_DURATION = 71 * 60 * 1000; // 71 min
private static final String ACTION_UPDATE_IDLE_MAINTENANCE_STATE =
"com.android.server.IdleMaintenanceService.action.UPDATE_IDLE_MAINTENANCE_STATE";
private static final String ACTION_FORCE_IDLE_MAINTENANCE =
"com.android.server.IdleMaintenanceService.action.FORCE_IDLE_MAINTENANCE";
static final int MSG_OP_COMPLETE = 1;
static final int MSG_IDLE_FINISHED = 2;
static final int MSG_TIMEOUT = 3;
// 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 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;
mAlarmService = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mHandler = new IdleHandler(mContext.getMainLooper());
mCallback = new IdleCallback();
Intent intent = new Intent(ACTION_UPDATE_IDLE_MAINTENANCE_STATE);
intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mUpdateIdleMaintenanceStatePendingIntent = PendingIntent.getBroadcast(mContext, 0,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
register(mHandler);
}
public void register(Handler handler) {
IntentFilter intentFilter = new IntentFilter();
// Alarm actions.
intentFilter.addAction(ACTION_UPDATE_IDLE_MAINTENANCE_STATE);
// Battery actions.
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
// Screen actions.
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
// Dream actions.
intentFilter.addAction(Intent.ACTION_DREAMING_STARTED);
intentFilter.addAction(Intent.ACTION_DREAMING_STOPPED);
mContext.registerReceiverAsUser(this, UserHandle.ALL,
intentFilter, null, mHandler);
intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_FORCE_IDLE_MAINTENANCE);
mContext.registerReceiverAsUser(this, UserHandle.ALL,
intentFilter, android.Manifest.permission.SET_ACTIVITY_WATCHER, mHandler);
}
private void scheduleUpdateIdleMaintenanceState(long delayMillis) {
final long triggetRealTimeMillis = SystemClock.elapsedRealtime() + delayMillis;
mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggetRealTimeMillis,
mUpdateIdleMaintenanceStatePendingIntent);
}
private void unscheduleUpdateIdleMaintenanceState() {
mAlarmService.cancel(mUpdateIdleMaintenanceStatePendingIntent);
}
private void updateIdleMaintenanceStateTm(boolean noisy) {
if (mIdleMaintenanceStarted) {
// Idle maintenance can be interrupted by user activity, or duration
// time out, or low battery.
final boolean batteryOk
= batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning();
if (!lastUserActivityPermitsIdleMaintenanceRunning() || !batteryOk) {
unscheduleUpdateIdleMaintenanceState();
mIdleMaintenanceStarted = false;
// 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 (!batteryOk) {
scheduleUpdateIdleMaintenanceState(
getNextIdleMaintenanceIntervalStartFromNow());
}
EventLogTags.writeIdleMaintenanceWindowFinish(SystemClock.elapsedRealtime(),
mLastUserActivityElapsedTimeMillis, mBatteryService.getBatteryLevel(),
isBatteryCharging() ? 1 : 0);
scheduleIdleFinishTm();
}
} else if (deviceStatePermitsIdleMaintenanceStart(noisy)
&& lastUserActivityPermitsIdleMaintenanceStart(noisy)
&& lastRunPermitsIdleMaintenanceStart(noisy)) {
// Now that we started idle maintenance, we should schedule another
// update for the moment when the idle maintenance times out.
scheduleUpdateIdleMaintenanceState(MAX_IDLE_MAINTENANCE_DURATION);
mIdleMaintenanceStarted = true;
EventLogTags.writeIdleMaintenanceWindowStart(SystemClock.elapsedRealtime(),
mLastUserActivityElapsedTimeMillis, mBatteryService.getBatteryLevel(),
isBatteryCharging() ? 1 : 0);
mLastIdleMaintenanceStartTimeMillis = SystemClock.elapsedRealtime();
startIdleMaintenanceTm();
} else if (lastUserActivityPermitsIdleMaintenanceStart(noisy)) {
if (lastRunPermitsIdleMaintenanceStart(noisy)) {
// The user does not use the device and we did not run maintenance in more
// than the min interval between runs, so schedule an update - maybe the
// battery will be charged latter.
scheduleUpdateIdleMaintenanceState(MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START);
} else {
// The user does not use the device but we have run maintenance in the min
// interval between runs, so schedule an update after the min interval ends.
scheduleUpdateIdleMaintenanceState(
getNextIdleMaintenanceIntervalStartFromNow());
}
}
}
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 boolean deviceStatePermitsIdleMaintenanceStart(boolean noisy) {
final int minBatteryLevel = isBatteryCharging()
? MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_CHARGING
: MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_NOT_CHARGING;
boolean allowed = (mLastUserActivityElapsedTimeMillis != LAST_USER_ACTIVITY_TIME_INVALID
&& mBatteryService.getBatteryLevel() > minBatteryLevel);
if (!allowed && noisy) {
Slog.i("IdleMaintenance", "Idle maintenance not allowed due to power");
}
return allowed;
}
private boolean lastUserActivityPermitsIdleMaintenanceStart(boolean noisy) {
// The last time the user poked the device is above the threshold.
boolean allowed = (mLastUserActivityElapsedTimeMillis != LAST_USER_ACTIVITY_TIME_INVALID
&& SystemClock.elapsedRealtime() - mLastUserActivityElapsedTimeMillis
> MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START);
if (!allowed && noisy) {
Slog.i("IdleMaintenance", "Idle maintenance not allowed due to last user activity");
}
return allowed;
}
private boolean lastRunPermitsIdleMaintenanceStart(boolean noisy) {
// Enough time passed since the last maintenance run.
boolean allowed = SystemClock.elapsedRealtime() - mLastIdleMaintenanceStartTimeMillis
> MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS;
if (!allowed && noisy) {
Slog.i("IdleMaintenance", "Idle maintenance not allowed due time since last");
}
return allowed;
}
private boolean lastUserActivityPermitsIdleMaintenanceRunning() {
// The user is not using the device.
return (mLastUserActivityElapsedTimeMillis != LAST_USER_ACTIVITY_TIME_INVALID);
}
private boolean batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning() {
// Battery not too low and the maintenance duration did not timeout.
return (mBatteryService.getBatteryLevel() > MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_RUNNING
&& mLastIdleMaintenanceStartTimeMillis + MAX_IDLE_MAINTENANCE_DURATION
> SystemClock.elapsedRealtime());
}
private boolean isBatteryCharging() {
return mBatteryService.getPlugType() > 0
&& mBatteryService.getInvalidCharger() == 0;
}
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) {
Log.i(TAG, intent.getAction());
}
String action = intent.getAction();
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
// We care about battery only if maintenance is in progress so we can
// stop it if battery is too low. Note that here we assume that the
// maintenance clients are properly holding a wake lock. We will
// refactor the maintenance to use services instead of intents for the
// next release. The only client for this for now is internal an holds
// a wake lock correctly.
if (mIdleMaintenanceStarted) {
updateIdleMaintenanceStateTm(false);
}
} else if (Intent.ACTION_SCREEN_ON.equals(action)
|| Intent.ACTION_DREAMING_STOPPED.equals(action)) {
mLastUserActivityElapsedTimeMillis = LAST_USER_ACTIVITY_TIME_INVALID;
// Unschedule any future updates since we already know that maintenance
// cannot be performed since the user is back.
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.
updateIdleMaintenanceStateTm(false);
} else if (Intent.ACTION_SCREEN_OFF.equals(action)
|| Intent.ACTION_DREAMING_STARTED.equals(action)) {
mLastUserActivityElapsedTimeMillis = SystemClock.elapsedRealtime();
// If screen went off/started dreaming, we may be able to start idle maintenance
// after the minimal user inactivity elapses. We schedule an alarm for when
// 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)) {
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;
updateIdleMaintenanceStateTm(true);
}
}
}