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 android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.os.BatteryManager;
24 import android.os.BatteryManagerInternal;
25 import android.os.SystemClock;
26 import android.os.UserHandle;
27 import android.util.ArraySet;
28 import android.util.Slog;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.server.LocalServices;
32 import com.android.server.job.JobSchedulerService;
33 import com.android.server.job.StateChangedListener;
34 
35 import java.io.PrintWriter;
36 
37 /**
38  * Simple controller that tracks whether the phone is charging or not. The phone is considered to
39  * be charging when it's been plugged in for more than two minutes, and the system has broadcast
40  * ACTION_BATTERY_OK.
41  */
42 public final class BatteryController extends StateController {
43     private static final String TAG = "JobScheduler.Batt";
44 
45     private static final Object sCreationLock = new Object();
46     private static volatile BatteryController sController;
47 
48     private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
49     private ChargingTracker mChargeTracker;
50 
get(JobSchedulerService taskManagerService)51     public static BatteryController get(JobSchedulerService taskManagerService) {
52         synchronized (sCreationLock) {
53             if (sController == null) {
54                 sController = new BatteryController(taskManagerService,
55                         taskManagerService.getContext(), taskManagerService.getLock());
56             }
57         }
58         return sController;
59     }
60 
61     @VisibleForTesting
getTracker()62     public ChargingTracker getTracker() {
63         return mChargeTracker;
64     }
65 
66     @VisibleForTesting
getForTesting(StateChangedListener stateChangedListener, Context context)67     public static BatteryController getForTesting(StateChangedListener stateChangedListener,
68                                            Context context) {
69         return new BatteryController(stateChangedListener, context, new Object());
70     }
71 
BatteryController(StateChangedListener stateChangedListener, Context context, Object lock)72     private BatteryController(StateChangedListener stateChangedListener, Context context,
73             Object lock) {
74         super(stateChangedListener, context, lock);
75         mChargeTracker = new ChargingTracker();
76         mChargeTracker.startTracking();
77     }
78 
79     @Override
maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob)80     public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
81         if (taskStatus.hasPowerConstraint()) {
82             mTrackedTasks.add(taskStatus);
83             taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY);
84             taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower());
85             taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow());
86         }
87     }
88 
89     @Override
maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate)90     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
91         if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
92             mTrackedTasks.remove(taskStatus);
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         boolean reportChange = false;
103         for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
104             final JobStatus ts = mTrackedTasks.valueAt(i);
105             boolean previous = ts.setChargingConstraintSatisfied(stablePower);
106             if (previous != stablePower) {
107                 reportChange = true;
108             }
109             previous = ts.setBatteryNotLowConstraintSatisfied(batteryNotLow);
110             if (previous != batteryNotLow) {
111                 reportChange = true;
112             }
113         }
114         if (stablePower || batteryNotLow) {
115             // If one of our conditions has been satisfied, always schedule any newly ready jobs.
116             mStateChangedListener.onRunJobNow(null);
117         } else if (reportChange) {
118             // Otherwise, just let the job scheduler know the state has changed and take care of it
119             // as it thinks is best.
120             mStateChangedListener.onControllerStateChanged();
121         }
122     }
123 
124     public final class ChargingTracker extends BroadcastReceiver {
125         /**
126          * Track whether we're "charging", where charging means that we're ready to commit to
127          * doing work.
128          */
129         private boolean mCharging;
130         /** Keep track of whether the battery is charged enough that we want to do work. */
131         private boolean mBatteryHealthy;
132         /** Sequence number of last broadcast. */
133         private int mLastBatterySeq = -1;
134 
135         private BroadcastReceiver mMonitor;
136 
ChargingTracker()137         public ChargingTracker() {
138         }
139 
startTracking()140         public void startTracking() {
141             IntentFilter filter = new IntentFilter();
142 
143             // Battery health.
144             filter.addAction(Intent.ACTION_BATTERY_LOW);
145             filter.addAction(Intent.ACTION_BATTERY_OKAY);
146             // Charging/not charging.
147             filter.addAction(BatteryManager.ACTION_CHARGING);
148             filter.addAction(BatteryManager.ACTION_DISCHARGING);
149             mContext.registerReceiver(this, filter);
150 
151             // Initialise tracker state.
152             BatteryManagerInternal batteryManagerInternal =
153                     LocalServices.getService(BatteryManagerInternal.class);
154             mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow();
155             mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
156         }
157 
setMonitorBatteryLocked(boolean enabled)158         public void setMonitorBatteryLocked(boolean enabled) {
159             if (enabled) {
160                 if (mMonitor == null) {
161                     mMonitor = new BroadcastReceiver() {
162                         @Override public void onReceive(Context context, Intent intent) {
163                             ChargingTracker.this.onReceive(context, intent);
164                         }
165                     };
166                     IntentFilter filter = new IntentFilter();
167                     filter.addAction(Intent.ACTION_BATTERY_CHANGED);
168                     mContext.registerReceiver(mMonitor, filter);
169                 }
170             } else {
171                 if (mMonitor != null) {
172                     mContext.unregisterReceiver(mMonitor);
173                     mMonitor = null;
174                 }
175             }
176         }
177 
isOnStablePower()178         public boolean isOnStablePower() {
179             return mCharging && mBatteryHealthy;
180         }
181 
isBatteryNotLow()182         public boolean isBatteryNotLow() {
183             return mBatteryHealthy;
184         }
185 
isMonitoring()186         public boolean isMonitoring() {
187             return mMonitor != null;
188         }
189 
getSeq()190         public int getSeq() {
191             return mLastBatterySeq;
192         }
193 
194         @Override
onReceive(Context context, Intent intent)195         public void onReceive(Context context, Intent intent) {
196             onReceiveInternal(intent);
197         }
198 
199         @VisibleForTesting
onReceiveInternal(Intent intent)200         public void onReceiveInternal(Intent intent) {
201             synchronized (mLock) {
202                 final String action = intent.getAction();
203                 if (Intent.ACTION_BATTERY_LOW.equals(action)) {
204                     if (DEBUG) {
205                         Slog.d(TAG, "Battery life too low to do work. @ "
206                                 + SystemClock.elapsedRealtime());
207                     }
208                     // If we get this action, the battery is discharging => it isn't plugged in so
209                     // there's no work to cancel. We track this variable for the case where it is
210                     // charging, but hasn't been for long enough to be healthy.
211                     mBatteryHealthy = false;
212                     maybeReportNewChargingStateLocked();
213                 } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
214                     if (DEBUG) {
215                         Slog.d(TAG, "Battery life healthy enough to do work. @ "
216                                 + SystemClock.elapsedRealtime());
217                     }
218                     mBatteryHealthy = true;
219                     maybeReportNewChargingStateLocked();
220                 } else if (BatteryManager.ACTION_CHARGING.equals(action)) {
221                     if (DEBUG) {
222                         Slog.d(TAG, "Received charging intent, fired @ "
223                                 + SystemClock.elapsedRealtime());
224                     }
225                     mCharging = true;
226                     maybeReportNewChargingStateLocked();
227                 } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
228                     if (DEBUG) {
229                         Slog.d(TAG, "Disconnected from power.");
230                     }
231                     mCharging = false;
232                     maybeReportNewChargingStateLocked();
233                 }
234                 mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE,
235                         mLastBatterySeq);
236             }
237         }
238     }
239 
240     @Override
dumpControllerStateLocked(PrintWriter pw, int filterUid)241     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
242         pw.print("Battery: stable power = ");
243         pw.print(mChargeTracker.isOnStablePower());
244         pw.print(", not low = ");
245         pw.println(mChargeTracker.isBatteryNotLow());
246         if (mChargeTracker.isMonitoring()) {
247             pw.print("MONITORING: seq=");
248             pw.println(mChargeTracker.getSeq());
249         }
250         pw.print("Tracking ");
251         pw.print(mTrackedTasks.size());
252         pw.println(":");
253         for (int i = 0; i < mTrackedTasks.size(); i++) {
254             final JobStatus js = mTrackedTasks.valueAt(i);
255             if (!js.shouldDump(filterUid)) {
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