1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.server.job.controllers;
18 
19 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
20 
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.os.BatteryManager;
26 import android.os.BatteryManagerInternal;
27 import android.os.UserHandle;
28 import android.util.ArraySet;
29 import android.util.Log;
30 import android.util.Slog;
31 import android.util.proto.ProtoOutputStream;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.util.IndentingPrintWriter;
35 import com.android.server.LocalServices;
36 import com.android.server.job.JobSchedulerService;
37 import com.android.server.job.StateControllerProto;
38 
39 import java.util.function.Predicate;
40 
41 /**
42  * Simple controller that tracks whether the phone is charging or not. The phone is considered to
43  * be charging when it's been plugged in for more than two minutes, and the system has broadcast
44  * ACTION_BATTERY_OK.
45  */
46 public final class BatteryController extends StateController {
47     private static final String TAG = "JobScheduler.Battery";
48     private static final boolean DEBUG = JobSchedulerService.DEBUG
49             || Log.isLoggable(TAG, Log.DEBUG);
50 
51     private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
52     private ChargingTracker mChargeTracker;
53 
54     @VisibleForTesting
getTracker()55     public ChargingTracker getTracker() {
56         return mChargeTracker;
57     }
58 
BatteryController(JobSchedulerService service)59     public BatteryController(JobSchedulerService service) {
60         super(service);
61         mChargeTracker = new ChargingTracker();
62         mChargeTracker.startTracking();
63     }
64 
65     @Override
maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob)66     public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
67         if (taskStatus.hasPowerConstraint()) {
68             mTrackedTasks.add(taskStatus);
69             taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY);
70             taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower());
71             taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow());
72         }
73     }
74 
75     @Override
maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate)76     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
77         if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
78             mTrackedTasks.remove(taskStatus);
79         }
80     }
81 
maybeReportNewChargingStateLocked()82     private void maybeReportNewChargingStateLocked() {
83         final boolean stablePower = mChargeTracker.isOnStablePower();
84         final boolean batteryNotLow = mChargeTracker.isBatteryNotLow();
85         if (DEBUG) {
86             Slog.d(TAG, "maybeReportNewChargingStateLocked: " + stablePower);
87         }
88         boolean reportChange = false;
89         for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
90             final JobStatus ts = mTrackedTasks.valueAt(i);
91             boolean previous = ts.setChargingConstraintSatisfied(stablePower);
92             if (previous != stablePower) {
93                 reportChange = true;
94             }
95             previous = ts.setBatteryNotLowConstraintSatisfied(batteryNotLow);
96             if (previous != batteryNotLow) {
97                 reportChange = true;
98             }
99         }
100         if (stablePower || batteryNotLow) {
101             // If one of our conditions has been satisfied, always schedule any newly ready jobs.
102             mStateChangedListener.onRunJobNow(null);
103         } else if (reportChange) {
104             // Otherwise, just let the job scheduler know the state has changed and take care of it
105             // as it thinks is best.
106             mStateChangedListener.onControllerStateChanged();
107         }
108     }
109 
110     public final class ChargingTracker extends BroadcastReceiver {
111         /**
112          * Track whether we're "charging", where charging means that we're ready to commit to
113          * doing work.
114          */
115         private boolean mCharging;
116         /** Keep track of whether the battery is charged enough that we want to do work. */
117         private boolean mBatteryHealthy;
118         /** Sequence number of last broadcast. */
119         private int mLastBatterySeq = -1;
120 
121         private BroadcastReceiver mMonitor;
122 
ChargingTracker()123         public ChargingTracker() {
124         }
125 
startTracking()126         public void startTracking() {
127             IntentFilter filter = new IntentFilter();
128 
129             // Battery health.
130             filter.addAction(Intent.ACTION_BATTERY_LOW);
131             filter.addAction(Intent.ACTION_BATTERY_OKAY);
132             // Charging/not charging.
133             filter.addAction(BatteryManager.ACTION_CHARGING);
134             filter.addAction(BatteryManager.ACTION_DISCHARGING);
135             mContext.registerReceiver(this, filter);
136 
137             // Initialise tracker state.
138             BatteryManagerInternal batteryManagerInternal =
139                     LocalServices.getService(BatteryManagerInternal.class);
140             mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow();
141             mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
142         }
143 
setMonitorBatteryLocked(boolean enabled)144         public void setMonitorBatteryLocked(boolean enabled) {
145             if (enabled) {
146                 if (mMonitor == null) {
147                     mMonitor = new BroadcastReceiver() {
148                         @Override public void onReceive(Context context, Intent intent) {
149                             ChargingTracker.this.onReceive(context, intent);
150                         }
151                     };
152                     IntentFilter filter = new IntentFilter();
153                     filter.addAction(Intent.ACTION_BATTERY_CHANGED);
154                     mContext.registerReceiver(mMonitor, filter);
155                 }
156             } else {
157                 if (mMonitor != null) {
158                     mContext.unregisterReceiver(mMonitor);
159                     mMonitor = null;
160                 }
161             }
162         }
163 
isOnStablePower()164         public boolean isOnStablePower() {
165             return mCharging && mBatteryHealthy;
166         }
167 
isBatteryNotLow()168         public boolean isBatteryNotLow() {
169             return mBatteryHealthy;
170         }
171 
isMonitoring()172         public boolean isMonitoring() {
173             return mMonitor != null;
174         }
175 
getSeq()176         public int getSeq() {
177             return mLastBatterySeq;
178         }
179 
180         @Override
onReceive(Context context, Intent intent)181         public void onReceive(Context context, Intent intent) {
182             onReceiveInternal(intent);
183         }
184 
185         @VisibleForTesting
onReceiveInternal(Intent intent)186         public void onReceiveInternal(Intent intent) {
187             synchronized (mLock) {
188                 final String action = intent.getAction();
189                 if (Intent.ACTION_BATTERY_LOW.equals(action)) {
190                     if (DEBUG) {
191                         Slog.d(TAG, "Battery life too low to do work. @ "
192                                 + sElapsedRealtimeClock.millis());
193                     }
194                     // If we get this action, the battery is discharging => it isn't plugged in so
195                     // there's no work to cancel. We track this variable for the case where it is
196                     // charging, but hasn't been for long enough to be healthy.
197                     mBatteryHealthy = false;
198                     maybeReportNewChargingStateLocked();
199                 } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
200                     if (DEBUG) {
201                         Slog.d(TAG, "Battery life healthy enough to do work. @ "
202                                 + sElapsedRealtimeClock.millis());
203                     }
204                     mBatteryHealthy = true;
205                     maybeReportNewChargingStateLocked();
206                 } else if (BatteryManager.ACTION_CHARGING.equals(action)) {
207                     if (DEBUG) {
208                         Slog.d(TAG, "Received charging intent, fired @ "
209                                 + sElapsedRealtimeClock.millis());
210                     }
211                     mCharging = true;
212                     maybeReportNewChargingStateLocked();
213                 } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
214                     if (DEBUG) {
215                         Slog.d(TAG, "Disconnected from power.");
216                     }
217                     mCharging = false;
218                     maybeReportNewChargingStateLocked();
219                 }
220                 mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE,
221                         mLastBatterySeq);
222             }
223         }
224     }
225 
226     @Override
dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)227     public void dumpControllerStateLocked(IndentingPrintWriter pw,
228             Predicate<JobStatus> predicate) {
229         pw.println("Stable power: " + mChargeTracker.isOnStablePower());
230         pw.println("Not low: " + mChargeTracker.isBatteryNotLow());
231 
232         if (mChargeTracker.isMonitoring()) {
233             pw.print("MONITORING: seq=");
234             pw.println(mChargeTracker.getSeq());
235         }
236         pw.println();
237 
238         for (int i = 0; i < mTrackedTasks.size(); i++) {
239             final JobStatus js = mTrackedTasks.valueAt(i);
240             if (!predicate.test(js)) {
241                 continue;
242             }
243             pw.print("#");
244             js.printUniqueId(pw);
245             pw.print(" from ");
246             UserHandle.formatUid(pw, js.getSourceUid());
247             pw.println();
248         }
249     }
250 
251     @Override
dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)252     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
253             Predicate<JobStatus> predicate) {
254         final long token = proto.start(fieldId);
255         final long mToken = proto.start(StateControllerProto.BATTERY);
256 
257         proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER,
258                 mChargeTracker.isOnStablePower());
259         proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW,
260                 mChargeTracker.isBatteryNotLow());
261 
262         proto.write(StateControllerProto.BatteryController.IS_MONITORING,
263                 mChargeTracker.isMonitoring());
264         proto.write(StateControllerProto.BatteryController.LAST_BROADCAST_SEQUENCE_NUMBER,
265                 mChargeTracker.getSeq());
266 
267         for (int i = 0; i < mTrackedTasks.size(); i++) {
268             final JobStatus js = mTrackedTasks.valueAt(i);
269             if (!predicate.test(js)) {
270                 continue;
271             }
272             final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS);
273             js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO);
274             proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID,
275                     js.getSourceUid());
276             proto.end(jsToken);
277         }
278 
279         proto.end(mToken);
280         proto.end(token);
281     }
282 }
283