blob: 46658ad33b854a804d4b98e72a1f6ee2ec04db80 [file] [log] [blame]
/*
* 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.job.controllers;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
import java.util.function.Predicate;
/**
* Simple controller that tracks whether the phone is charging or not. The phone is considered to
* be charging when it's been plugged in for more than two minutes, and the system has broadcast
* ACTION_BATTERY_OK.
*/
public final class BatteryController extends StateController {
private static final String TAG = "JobScheduler.Battery";
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
private ChargingTracker mChargeTracker;
@VisibleForTesting
public ChargingTracker getTracker() {
return mChargeTracker;
}
public BatteryController(JobSchedulerService service) {
super(service);
mChargeTracker = new ChargingTracker();
mChargeTracker.startTracking();
}
@Override
public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
if (taskStatus.hasPowerConstraint()) {
mTrackedTasks.add(taskStatus);
taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY);
taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower());
taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow());
}
}
@Override
public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
mTrackedTasks.remove(taskStatus);
}
}
private void maybeReportNewChargingStateLocked() {
final boolean stablePower = mChargeTracker.isOnStablePower();
final boolean batteryNotLow = mChargeTracker.isBatteryNotLow();
if (DEBUG) {
Slog.d(TAG, "maybeReportNewChargingStateLocked: " + stablePower);
}
boolean reportChange = false;
for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
final JobStatus ts = mTrackedTasks.valueAt(i);
boolean previous = ts.setChargingConstraintSatisfied(stablePower);
if (previous != stablePower) {
reportChange = true;
}
previous = ts.setBatteryNotLowConstraintSatisfied(batteryNotLow);
if (previous != batteryNotLow) {
reportChange = true;
}
}
if (stablePower || batteryNotLow) {
// If one of our conditions has been satisfied, always schedule any newly ready jobs.
mStateChangedListener.onRunJobNow(null);
} else if (reportChange) {
// Otherwise, just let the job scheduler know the state has changed and take care of it
// as it thinks is best.
mStateChangedListener.onControllerStateChanged();
}
}
public final class ChargingTracker extends BroadcastReceiver {
/**
* Track whether we're "charging", where charging means that we're ready to commit to
* doing work.
*/
private boolean mCharging;
/** Keep track of whether the battery is charged enough that we want to do work. */
private boolean mBatteryHealthy;
/** Sequence number of last broadcast. */
private int mLastBatterySeq = -1;
private BroadcastReceiver mMonitor;
public ChargingTracker() {
}
public void startTracking() {
IntentFilter filter = new IntentFilter();
// Battery health.
filter.addAction(Intent.ACTION_BATTERY_LOW);
filter.addAction(Intent.ACTION_BATTERY_OKAY);
// Charging/not charging.
filter.addAction(BatteryManager.ACTION_CHARGING);
filter.addAction(BatteryManager.ACTION_DISCHARGING);
mContext.registerReceiver(this, filter);
// Initialise tracker state.
BatteryManagerInternal batteryManagerInternal =
LocalServices.getService(BatteryManagerInternal.class);
mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow();
mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
}
public void setMonitorBatteryLocked(boolean enabled) {
if (enabled) {
if (mMonitor == null) {
mMonitor = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
ChargingTracker.this.onReceive(context, intent);
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
mContext.registerReceiver(mMonitor, filter);
}
} else {
if (mMonitor != null) {
mContext.unregisterReceiver(mMonitor);
mMonitor = null;
}
}
}
public boolean isOnStablePower() {
return mCharging && mBatteryHealthy;
}
public boolean isBatteryNotLow() {
return mBatteryHealthy;
}
public boolean isMonitoring() {
return mMonitor != null;
}
public int getSeq() {
return mLastBatterySeq;
}
@Override
public void onReceive(Context context, Intent intent) {
onReceiveInternal(intent);
}
@VisibleForTesting
public void onReceiveInternal(Intent intent) {
synchronized (mLock) {
final String action = intent.getAction();
if (Intent.ACTION_BATTERY_LOW.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Battery life too low to do work. @ "
+ sElapsedRealtimeClock.millis());
}
// If we get this action, the battery is discharging => it isn't plugged in so
// there's no work to cancel. We track this variable for the case where it is
// charging, but hasn't been for long enough to be healthy.
mBatteryHealthy = false;
maybeReportNewChargingStateLocked();
} else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Battery life healthy enough to do work. @ "
+ sElapsedRealtimeClock.millis());
}
mBatteryHealthy = true;
maybeReportNewChargingStateLocked();
} else if (BatteryManager.ACTION_CHARGING.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Received charging intent, fired @ "
+ sElapsedRealtimeClock.millis());
}
mCharging = true;
maybeReportNewChargingStateLocked();
} else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Disconnected from power.");
}
mCharging = false;
maybeReportNewChargingStateLocked();
}
mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE,
mLastBatterySeq);
}
}
}
@Override
public void dumpControllerStateLocked(IndentingPrintWriter pw,
Predicate<JobStatus> predicate) {
pw.println("Stable power: " + mChargeTracker.isOnStablePower());
pw.println("Not low: " + mChargeTracker.isBatteryNotLow());
if (mChargeTracker.isMonitoring()) {
pw.print("MONITORING: seq=");
pw.println(mChargeTracker.getSeq());
}
pw.println();
for (int i = 0; i < mTrackedTasks.size(); i++) {
final JobStatus js = mTrackedTasks.valueAt(i);
if (!predicate.test(js)) {
continue;
}
pw.print("#");
js.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, js.getSourceUid());
pw.println();
}
}
@Override
public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
Predicate<JobStatus> predicate) {
final long token = proto.start(fieldId);
final long mToken = proto.start(StateControllerProto.BATTERY);
proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER,
mChargeTracker.isOnStablePower());
proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW,
mChargeTracker.isBatteryNotLow());
proto.write(StateControllerProto.BatteryController.IS_MONITORING,
mChargeTracker.isMonitoring());
proto.write(StateControllerProto.BatteryController.LAST_BROADCAST_SEQUENCE_NUMBER,
mChargeTracker.getSeq());
for (int i = 0; i < mTrackedTasks.size(); i++) {
final JobStatus js = mTrackedTasks.valueAt(i);
if (!predicate.test(js)) {
continue;
}
final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS);
js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO);
proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID,
js.getSourceUid());
proto.end(jsToken);
}
proto.end(mToken);
proto.end(token);
}
}