BatteryController.java revision 2139276ce8b54aba5faa858ca69ed5f01445c269
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
17package com.android.server.job.controllers;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.os.BatteryManager;
26import android.os.BatteryManagerInternal;
27import android.os.SystemClock;
28import android.util.Slog;
29
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.server.LocalServices;
32import com.android.server.job.JobSchedulerService;
33import com.android.server.job.StateChangedListener;
34
35import java.io.PrintWriter;
36import java.util.ArrayList;
37import java.util.Iterator;
38import java.util.List;
39
40/**
41 * Simple controller that tracks whether the phone is charging or not. The phone is considered to
42 * be charging when it's been plugged in for more than two minutes, and the system has broadcast
43 * ACTION_BATTERY_OK.
44 */
45public class BatteryController extends StateController {
46    private static final String TAG = "JobScheduler.Batt";
47
48    private static final Object sCreationLock = new Object();
49    private static volatile BatteryController sController;
50    private static final String ACTION_CHARGING_STABLE =
51            "com.android.server.task.controllers.BatteryController.ACTION_CHARGING_STABLE";
52    /** Wait this long after phone is plugged in before doing any work. */
53    private static final long STABLE_CHARGING_THRESHOLD_MILLIS = 2 * 60 * 1000; // 2 minutes.
54
55    private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
56    private ChargingTracker mChargeTracker;
57
58    public static BatteryController get(JobSchedulerService taskManagerService) {
59        synchronized (sCreationLock) {
60            if (sController == null) {
61                sController = new BatteryController(taskManagerService,
62                        taskManagerService.getContext());
63            }
64        }
65        return sController;
66    }
67
68    @VisibleForTesting
69    public ChargingTracker getTracker() {
70        return mChargeTracker;
71    }
72
73    @VisibleForTesting
74    public static BatteryController getForTesting(StateChangedListener stateChangedListener,
75                                           Context context) {
76        return new BatteryController(stateChangedListener, context);
77    }
78
79    private BatteryController(StateChangedListener stateChangedListener, Context context) {
80        super(stateChangedListener, context);
81        mChargeTracker = new ChargingTracker();
82        mChargeTracker.startTracking();
83    }
84
85    @Override
86    public void maybeStartTrackingJob(JobStatus taskStatus) {
87        if (taskStatus.hasChargingConstraint()) {
88            final boolean isOnStablePower = mChargeTracker.isOnStablePower();
89            synchronized (mTrackedTasks) {
90                mTrackedTasks.add(taskStatus);
91                taskStatus.chargingConstraintSatisfied.set(isOnStablePower);
92            }
93            if (isOnStablePower) {
94                mStateChangedListener.onControllerStateChanged();
95            }
96        }
97    }
98
99    @Override
100    public void maybeStopTrackingJob(JobStatus taskStatus) {
101        if (taskStatus.hasChargingConstraint()) {
102            synchronized (mTrackedTasks) {
103                mTrackedTasks.remove(taskStatus);
104            }
105        }
106    }
107
108    private void maybeReportNewChargingState() {
109        final boolean stablePower = mChargeTracker.isOnStablePower();
110        if (DEBUG) {
111            Slog.d(TAG, "maybeReportNewChargingState: " + stablePower);
112        }
113        boolean reportChange = false;
114        synchronized (mTrackedTasks) {
115            for (JobStatus ts : mTrackedTasks) {
116                boolean previous = ts.chargingConstraintSatisfied.getAndSet(stablePower);
117                if (previous != stablePower) {
118                    reportChange = true;
119                }
120            }
121        }
122        if (reportChange) {
123            mStateChangedListener.onControllerStateChanged();
124        }
125    }
126
127    public class ChargingTracker extends BroadcastReceiver {
128        private final AlarmManager mAlarm;
129        private final PendingIntent mStableChargingTriggerIntent;
130        /**
131         * Track whether we're "charging", where charging means that we're ready to commit to
132         * doing work.
133         */
134        private boolean mCharging;
135        /** Keep track of whether the battery is charged enough that we want to do work. */
136        private boolean mBatteryHealthy;
137
138        public ChargingTracker() {
139            mAlarm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
140            Intent intent = new Intent(ACTION_CHARGING_STABLE);
141            mStableChargingTriggerIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
142        }
143
144        public void startTracking() {
145            IntentFilter filter = new IntentFilter();
146
147            // Battery health.
148            filter.addAction(Intent.ACTION_BATTERY_LOW);
149            filter.addAction(Intent.ACTION_BATTERY_OKAY);
150            // Charging/not charging.
151            filter.addAction(Intent.ACTION_POWER_CONNECTED);
152            filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
153            // Charging stable.
154            filter.addAction(ACTION_CHARGING_STABLE);
155            mContext.registerReceiver(this, filter);
156
157            // Initialise tracker state.
158            BatteryManagerInternal batteryManagerInternal =
159                    LocalServices.getService(BatteryManagerInternal.class);
160            mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow();
161            mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
162        }
163
164        boolean isOnStablePower() {
165            return mCharging && mBatteryHealthy;
166        }
167
168        @Override
169        public void onReceive(Context context, Intent intent) {
170            onReceiveInternal(intent);
171        }
172
173        @VisibleForTesting
174        public void onReceiveInternal(Intent intent) {
175            final String action = intent.getAction();
176            if (Intent.ACTION_BATTERY_LOW.equals(action)) {
177                if (DEBUG) {
178                    Slog.d(TAG, "Battery life too low to do work. @ "
179                            + SystemClock.elapsedRealtime());
180                }
181                // If we get this action, the battery is discharging => it isn't plugged in so
182                // there's no work to cancel. We track this variable for the case where it is
183                // charging, but hasn't been for long enough to be healthy.
184                mBatteryHealthy = false;
185            } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
186                if (DEBUG) {
187                    Slog.d(TAG, "Battery life healthy enough to do work. @ "
188                            + SystemClock.elapsedRealtime());
189                }
190                mBatteryHealthy = true;
191                maybeReportNewChargingState();
192            } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
193                if (DEBUG) {
194                    Slog.d(TAG, "Received charging intent, setting alarm for "
195                            + STABLE_CHARGING_THRESHOLD_MILLIS);
196                }
197                // Set up an alarm for ACTION_CHARGING_STABLE - we don't want to kick off tasks
198                // here if the user unplugs the phone immediately.
199                mAlarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
200                        SystemClock.elapsedRealtime() + STABLE_CHARGING_THRESHOLD_MILLIS,
201                        mStableChargingTriggerIntent);
202                mCharging = true;
203            } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
204                if (DEBUG) {
205                    Slog.d(TAG, "Disconnected from power, cancelling any set alarms.");
206                }
207                // If an alarm is set, breathe a sigh of relief and cancel it - crisis averted.
208                mAlarm.cancel(mStableChargingTriggerIntent);
209                mCharging = false;
210                maybeReportNewChargingState();
211            }else if (ACTION_CHARGING_STABLE.equals(action)) {
212                // Here's where we actually do the notify for a task being ready.
213                if (DEBUG) {
214                    Slog.d(TAG, "Battery connected fired @ " + SystemClock.elapsedRealtime()
215                            + " charging: " + mCharging);
216                }
217                if (mCharging) {  // Should never receive this intent if mCharging is false.
218                    maybeReportNewChargingState();
219                }
220            }
221        }
222    }
223
224    @Override
225    public void dumpControllerState(PrintWriter pw) {
226        pw.println("Batt.");
227        pw.println("Stable power: " + mChargeTracker.isOnStablePower());
228        synchronized (mTrackedTasks) {
229            Iterator<JobStatus> it = mTrackedTasks.iterator();
230            if (it.hasNext()) {
231                pw.print(String.valueOf(it.next().hashCode()));
232            }
233            while (it.hasNext()) {
234                pw.print("," + String.valueOf(it.next().hashCode()));
235            }
236            pw.println();
237        }
238    }
239}
240