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.IndentingPrintWriter;
30 import android.util.Log;
31 import android.util.Slog;
32 import android.util.proto.ProtoOutputStream;
33 
34 import com.android.internal.annotations.VisibleForTesting;
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 RestrictingController {
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             final long nowElapsed = sElapsedRealtimeClock.millis();
69             mTrackedTasks.add(taskStatus);
70             taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY);
71             taskStatus.setChargingConstraintSatisfied(nowElapsed, mChargeTracker.isOnStablePower());
72             taskStatus.setBatteryNotLowConstraintSatisfied(
73                     nowElapsed, mChargeTracker.isBatteryNotLow());
74         }
75     }
76 
77     @Override
startTrackingRestrictedJobLocked(JobStatus jobStatus)78     public void startTrackingRestrictedJobLocked(JobStatus jobStatus) {
79         maybeStartTrackingJobLocked(jobStatus, null);
80     }
81 
82     @Override
maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate)83     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
84         if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
85             mTrackedTasks.remove(taskStatus);
86         }
87     }
88 
89     @Override
stopTrackingRestrictedJobLocked(JobStatus jobStatus)90     public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
91         if (!jobStatus.hasPowerConstraint()) {
92             maybeStopTrackingJobLocked(jobStatus, null, false);
93         }
94     }
95 
maybeReportNewChargingStateLocked()96     private void maybeReportNewChargingStateLocked() {
97         final boolean stablePower = mChargeTracker.isOnStablePower();
98         final boolean batteryNotLow = mChargeTracker.isBatteryNotLow();
99         if (DEBUG) {
100             Slog.d(TAG, "maybeReportNewChargingStateLocked: " + stablePower);
101         }
102         final long nowElapsed = sElapsedRealtimeClock.millis();
103         boolean reportChange = false;
104         for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
105             final JobStatus ts = mTrackedTasks.valueAt(i);
106             boolean previous = ts.setChargingConstraintSatisfied(nowElapsed, stablePower);
107             if (previous != stablePower) {
108                 reportChange = true;
109             }
110             previous = ts.setBatteryNotLowConstraintSatisfied(nowElapsed, batteryNotLow);
111             if (previous != batteryNotLow) {
112                 reportChange = true;
113             }
114         }
115         if (stablePower || batteryNotLow) {
116             // If one of our conditions has been satisfied, always schedule any newly ready jobs.
117             mStateChangedListener.onRunJobNow(null);
118         } else if (reportChange) {
119             // Otherwise, just let the job scheduler know the state has changed and take care of it
120             // as it thinks is best.
121             mStateChangedListener.onControllerStateChanged();
122         }
123     }
124 
125     public final class ChargingTracker extends BroadcastReceiver {
126         /**
127          * Track whether we're "charging", where charging means that we're ready to commit to
128          * doing work.
129          */
130         private boolean mCharging;
131         /** Keep track of whether the battery is charged enough that we want to do work. */
132         private boolean mBatteryHealthy;
133         /** Sequence number of last broadcast. */
134         private int mLastBatterySeq = -1;
135 
136         private BroadcastReceiver mMonitor;
137 
ChargingTracker()138         public ChargingTracker() {
139         }
140 
startTracking()141         public void startTracking() {
142             IntentFilter filter = new IntentFilter();
143 
144             // Battery health.
145             filter.addAction(Intent.ACTION_BATTERY_LOW);
146             filter.addAction(Intent.ACTION_BATTERY_OKAY);
147             // Charging/not charging.
148             filter.addAction(BatteryManager.ACTION_CHARGING);
149             filter.addAction(BatteryManager.ACTION_DISCHARGING);
150             mContext.registerReceiver(this, filter);
151 
152             // Initialise tracker state.
153             BatteryManagerInternal batteryManagerInternal =
154                     LocalServices.getService(BatteryManagerInternal.class);
155             mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow();
156             mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
157         }
158 
setMonitorBatteryLocked(boolean enabled)159         public void setMonitorBatteryLocked(boolean enabled) {
160             if (enabled) {
161                 if (mMonitor == null) {
162                     mMonitor = new BroadcastReceiver() {
163                         @Override public void onReceive(Context context, Intent intent) {
164                             ChargingTracker.this.onReceive(context, intent);
165                         }
166                     };
167                     IntentFilter filter = new IntentFilter();
168                     filter.addAction(Intent.ACTION_BATTERY_CHANGED);
169                     mContext.registerReceiver(mMonitor, filter);
170                 }
171             } else {
172                 if (mMonitor != null) {
173                     mContext.unregisterReceiver(mMonitor);
174                     mMonitor = null;
175                 }
176             }
177         }
178 
isOnStablePower()179         public boolean isOnStablePower() {
180             return mCharging && mBatteryHealthy;
181         }
182 
isBatteryNotLow()183         public boolean isBatteryNotLow() {
184             return mBatteryHealthy;
185         }
186 
isMonitoring()187         public boolean isMonitoring() {
188             return mMonitor != null;
189         }
190 
getSeq()191         public int getSeq() {
192             return mLastBatterySeq;
193         }
194 
195         @Override
onReceive(Context context, Intent intent)196         public void onReceive(Context context, Intent intent) {
197             onReceiveInternal(intent);
198         }
199 
200         @VisibleForTesting
onReceiveInternal(Intent intent)201         public void onReceiveInternal(Intent intent) {
202             synchronized (mLock) {
203                 final String action = intent.getAction();
204                 if (Intent.ACTION_BATTERY_LOW.equals(action)) {
205                     if (DEBUG) {
206                         Slog.d(TAG, "Battery life too low to do work. @ "
207                                 + sElapsedRealtimeClock.millis());
208                     }
209                     // If we get this action, the battery is discharging => it isn't plugged in so
210                     // there's no work to cancel. We track this variable for the case where it is
211                     // charging, but hasn't been for long enough to be healthy.
212                     mBatteryHealthy = false;
213                     maybeReportNewChargingStateLocked();
214                 } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
215                     if (DEBUG) {
216                         Slog.d(TAG, "Battery life healthy enough to do work. @ "
217                                 + sElapsedRealtimeClock.millis());
218                     }
219                     mBatteryHealthy = true;
220                     maybeReportNewChargingStateLocked();
221                 } else if (BatteryManager.ACTION_CHARGING.equals(action)) {
222                     if (DEBUG) {
223                         Slog.d(TAG, "Received charging intent, fired @ "
224                                 + sElapsedRealtimeClock.millis());
225                     }
226                     mCharging = true;
227                     maybeReportNewChargingStateLocked();
228                 } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
229                     if (DEBUG) {
230                         Slog.d(TAG, "Disconnected from power.");
231                     }
232                     mCharging = false;
233                     maybeReportNewChargingStateLocked();
234                 }
235                 mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE,
236                         mLastBatterySeq);
237             }
238         }
239     }
240 
241     @Override
dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)242     public void dumpControllerStateLocked(IndentingPrintWriter pw,
243             Predicate<JobStatus> predicate) {
244         pw.println("Stable power: " + mChargeTracker.isOnStablePower());
245         pw.println("Not low: " + mChargeTracker.isBatteryNotLow());
246 
247         if (mChargeTracker.isMonitoring()) {
248             pw.print("MONITORING: seq=");
249             pw.println(mChargeTracker.getSeq());
250         }
251         pw.println();
252 
253         for (int i = 0; i < mTrackedTasks.size(); i++) {
254             final JobStatus js = mTrackedTasks.valueAt(i);
255             if (!predicate.test(js)) {
256                 continue;
257             }
258             pw.print("#");
259             js.printUniqueId(pw);
260             pw.print(" from ");
261             UserHandle.formatUid(pw, js.getSourceUid());
262             pw.println();
263         }
264     }
265 
266     @Override
dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)267     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
268             Predicate<JobStatus> predicate) {
269         final long token = proto.start(fieldId);
270         final long mToken = proto.start(StateControllerProto.BATTERY);
271 
272         proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER,
273                 mChargeTracker.isOnStablePower());
274         proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW,
275                 mChargeTracker.isBatteryNotLow());
276 
277         proto.write(StateControllerProto.BatteryController.IS_MONITORING,
278                 mChargeTracker.isMonitoring());
279         proto.write(StateControllerProto.BatteryController.LAST_BROADCAST_SEQUENCE_NUMBER,
280                 mChargeTracker.getSeq());
281 
282         for (int i = 0; i < mTrackedTasks.size(); i++) {
283             final JobStatus js = mTrackedTasks.valueAt(i);
284             if (!predicate.test(js)) {
285                 continue;
286             }
287             final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS);
288             js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO);
289             proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID,
290                     js.getSourceUid());
291             proto.end(jsToken);
292         }
293 
294         proto.end(mToken);
295         proto.end(token);
296     }
297 }
298