1/*
2 * Copyright (C) 2008 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.systemui.power;
18
19import android.app.NotificationManager;
20import android.content.BroadcastReceiver;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.res.Resources;
26import android.database.ContentObserver;
27import android.os.BatteryManager;
28import android.os.Handler;
29import android.os.HardwarePropertiesManager;
30import android.os.PowerManager;
31import android.os.SystemClock;
32import android.os.UserHandle;
33import android.provider.Settings;
34import android.text.TextUtils;
35import android.text.format.DateUtils;
36import android.util.Log;
37import android.util.Slog;
38import com.android.internal.logging.MetricsLogger;
39import com.android.systemui.R;
40import com.android.systemui.SystemUI;
41import com.android.systemui.statusbar.phone.StatusBar;
42import java.io.FileDescriptor;
43import java.io.PrintWriter;
44import java.util.Arrays;
45
46public class PowerUI extends SystemUI {
47    static final String TAG = "PowerUI";
48    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
49    private static final long TEMPERATURE_INTERVAL = 30 * DateUtils.SECOND_IN_MILLIS;
50    private static final long TEMPERATURE_LOGGING_INTERVAL = DateUtils.HOUR_IN_MILLIS;
51    private static final int MAX_RECENT_TEMPS = 125; // TEMPERATURE_LOGGING_INTERVAL plus a buffer
52
53    private final Handler mHandler = new Handler();
54    private final Receiver mReceiver = new Receiver();
55
56    private PowerManager mPowerManager;
57    private HardwarePropertiesManager mHardwarePropertiesManager;
58    private WarningsUI mWarnings;
59    private int mBatteryLevel = 100;
60    private int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN;
61    private int mPlugType = 0;
62    private int mInvalidCharger = 0;
63
64    private int mLowBatteryAlertCloseLevel;
65    private final int[] mLowBatteryReminderLevels = new int[2];
66
67    private long mScreenOffTime = -1;
68
69    private float mThresholdTemp;
70    private float[] mRecentTemps = new float[MAX_RECENT_TEMPS];
71    private int mNumTemps;
72    private long mNextLogTime;
73
74    public void start() {
75        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
76        mHardwarePropertiesManager = (HardwarePropertiesManager)
77                mContext.getSystemService(Context.HARDWARE_PROPERTIES_SERVICE);
78        mScreenOffTime = mPowerManager.isScreenOn() ? -1 : SystemClock.elapsedRealtime();
79        mWarnings = new PowerNotificationWarnings(
80                mContext,
81                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE),
82                getComponent(StatusBar.class));
83
84        ContentObserver obs = new ContentObserver(mHandler) {
85            @Override
86            public void onChange(boolean selfChange) {
87                updateBatteryWarningLevels();
88            }
89        };
90        final ContentResolver resolver = mContext.getContentResolver();
91        resolver.registerContentObserver(Settings.Global.getUriFor(
92                Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL),
93                false, obs, UserHandle.USER_ALL);
94        updateBatteryWarningLevels();
95        mReceiver.init();
96
97        // Check to see if we need to let the user know that the phone previously shut down due
98        // to the temperature being too high.
99        showThermalShutdownDialog();
100
101        initTemperatureWarning();
102    }
103
104    void updateBatteryWarningLevels() {
105        int critLevel = mContext.getResources().getInteger(
106                com.android.internal.R.integer.config_criticalBatteryWarningLevel);
107
108        final ContentResolver resolver = mContext.getContentResolver();
109        int defWarnLevel = mContext.getResources().getInteger(
110                com.android.internal.R.integer.config_lowBatteryWarningLevel);
111        int warnLevel = Settings.Global.getInt(resolver,
112                Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, defWarnLevel);
113        if (warnLevel == 0) {
114            warnLevel = defWarnLevel;
115        }
116        if (warnLevel < critLevel) {
117            warnLevel = critLevel;
118        }
119
120        mLowBatteryReminderLevels[0] = warnLevel;
121        mLowBatteryReminderLevels[1] = critLevel;
122        mLowBatteryAlertCloseLevel = mLowBatteryReminderLevels[0]
123                + mContext.getResources().getInteger(
124                        com.android.internal.R.integer.config_lowBatteryCloseWarningBump);
125    }
126
127    /**
128     * Buckets the battery level.
129     *
130     * The code in this function is a little weird because I couldn't comprehend
131     * the bucket going up when the battery level was going down. --joeo
132     *
133     * 1 means that the battery is "ok"
134     * 0 means that the battery is between "ok" and what we should warn about.
135     * less than 0 means that the battery is low
136     */
137    private int findBatteryLevelBucket(int level) {
138        if (level >= mLowBatteryAlertCloseLevel) {
139            return 1;
140        }
141        if (level > mLowBatteryReminderLevels[0]) {
142            return 0;
143        }
144        final int N = mLowBatteryReminderLevels.length;
145        for (int i=N-1; i>=0; i--) {
146            if (level <= mLowBatteryReminderLevels[i]) {
147                return -1-i;
148            }
149        }
150        throw new RuntimeException("not possible!");
151    }
152
153    private final class Receiver extends BroadcastReceiver {
154
155        public void init() {
156            // Register for Intent broadcasts for...
157            IntentFilter filter = new IntentFilter();
158            filter.addAction(Intent.ACTION_BATTERY_CHANGED);
159            filter.addAction(Intent.ACTION_SCREEN_OFF);
160            filter.addAction(Intent.ACTION_SCREEN_ON);
161            filter.addAction(Intent.ACTION_USER_SWITCHED);
162            mContext.registerReceiver(this, filter, null, mHandler);
163        }
164
165        @Override
166        public void onReceive(Context context, Intent intent) {
167            String action = intent.getAction();
168            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
169                final int oldBatteryLevel = mBatteryLevel;
170                mBatteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 100);
171                final int oldBatteryStatus = mBatteryStatus;
172                mBatteryStatus = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
173                        BatteryManager.BATTERY_STATUS_UNKNOWN);
174                final int oldPlugType = mPlugType;
175                mPlugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 1);
176                final int oldInvalidCharger = mInvalidCharger;
177                mInvalidCharger = intent.getIntExtra(BatteryManager.EXTRA_INVALID_CHARGER, 0);
178
179                final boolean plugged = mPlugType != 0;
180                final boolean oldPlugged = oldPlugType != 0;
181
182                int oldBucket = findBatteryLevelBucket(oldBatteryLevel);
183                int bucket = findBatteryLevelBucket(mBatteryLevel);
184
185                if (DEBUG) {
186                    Slog.d(TAG, "buckets   ....." + mLowBatteryAlertCloseLevel
187                            + " .. " + mLowBatteryReminderLevels[0]
188                            + " .. " + mLowBatteryReminderLevels[1]);
189                    Slog.d(TAG, "level          " + oldBatteryLevel + " --> " + mBatteryLevel);
190                    Slog.d(TAG, "status         " + oldBatteryStatus + " --> " + mBatteryStatus);
191                    Slog.d(TAG, "plugType       " + oldPlugType + " --> " + mPlugType);
192                    Slog.d(TAG, "invalidCharger " + oldInvalidCharger + " --> " + mInvalidCharger);
193                    Slog.d(TAG, "bucket         " + oldBucket + " --> " + bucket);
194                    Slog.d(TAG, "plugged        " + oldPlugged + " --> " + plugged);
195                }
196
197                mWarnings.update(mBatteryLevel, bucket, mScreenOffTime);
198                if (oldInvalidCharger == 0 && mInvalidCharger != 0) {
199                    Slog.d(TAG, "showing invalid charger warning");
200                    mWarnings.showInvalidChargerWarning();
201                    return;
202                } else if (oldInvalidCharger != 0 && mInvalidCharger == 0) {
203                    mWarnings.dismissInvalidChargerWarning();
204                } else if (mWarnings.isInvalidChargerWarningShowing()) {
205                    // if invalid charger is showing, don't show low battery
206                    return;
207                }
208
209                boolean isPowerSaver = mPowerManager.isPowerSaveMode();
210                if (!plugged
211                        && !isPowerSaver
212                        && (bucket < oldBucket || oldPlugged)
213                        && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
214                        && bucket < 0) {
215                    // only play SFX when the dialog comes up or the bucket changes
216                    final boolean playSound = bucket != oldBucket || oldPlugged;
217                    mWarnings.showLowBatteryWarning(playSound);
218                } else if (isPowerSaver || plugged || (bucket > oldBucket && bucket > 0)) {
219                    mWarnings.dismissLowBatteryWarning();
220                } else {
221                    mWarnings.updateLowBatteryWarning();
222                }
223            } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
224                mScreenOffTime = SystemClock.elapsedRealtime();
225            } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
226                mScreenOffTime = -1;
227            } else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
228                mWarnings.userSwitched();
229            } else {
230                Slog.w(TAG, "unknown intent: " + intent);
231            }
232        }
233    };
234
235    private void initTemperatureWarning() {
236        ContentResolver resolver = mContext.getContentResolver();
237        Resources resources = mContext.getResources();
238        if (Settings.Global.getInt(resolver, Settings.Global.SHOW_TEMPERATURE_WARNING,
239                resources.getInteger(R.integer.config_showTemperatureWarning)) == 0) {
240            return;
241        }
242
243        mThresholdTemp = Settings.Global.getFloat(resolver, Settings.Global.WARNING_TEMPERATURE,
244                resources.getInteger(R.integer.config_warningTemperature));
245
246        if (mThresholdTemp < 0f) {
247            // Get the throttling temperature. No need to check if we're not throttling.
248            float[] throttlingTemps = mHardwarePropertiesManager.getDeviceTemperatures(
249                    HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN,
250                    HardwarePropertiesManager.TEMPERATURE_THROTTLING);
251            if (throttlingTemps == null
252                    || throttlingTemps.length == 0
253                    || throttlingTemps[0] == HardwarePropertiesManager.UNDEFINED_TEMPERATURE) {
254                return;
255            }
256            mThresholdTemp = throttlingTemps[0];
257        }
258        setNextLogTime();
259
260        // We have passed all of the checks, start checking the temp
261        updateTemperatureWarning();
262    }
263
264    private void showThermalShutdownDialog() {
265        if (mPowerManager.getLastShutdownReason()
266                == PowerManager.SHUTDOWN_REASON_THERMAL_SHUTDOWN) {
267            mWarnings.showThermalShutdownWarning();
268        }
269    }
270
271    private void updateTemperatureWarning() {
272        float[] temps = mHardwarePropertiesManager.getDeviceTemperatures(
273                HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN,
274                HardwarePropertiesManager.TEMPERATURE_CURRENT);
275        if (temps.length != 0) {
276            float temp = temps[0];
277            mRecentTemps[mNumTemps++] = temp;
278
279            StatusBar statusBar = getComponent(StatusBar.class);
280            if (statusBar != null && !statusBar.isDeviceInVrMode()
281                    && temp >= mThresholdTemp) {
282                logAtTemperatureThreshold(temp);
283                mWarnings.showHighTemperatureWarning();
284            } else {
285                mWarnings.dismissHighTemperatureWarning();
286            }
287        }
288
289        logTemperatureStats();
290
291        mHandler.postDelayed(this::updateTemperatureWarning, TEMPERATURE_INTERVAL);
292    }
293
294    private void logAtTemperatureThreshold(float temp) {
295        StringBuilder sb = new StringBuilder();
296        sb.append("currentTemp=").append(temp)
297                .append(",thresholdTemp=").append(mThresholdTemp)
298                .append(",batteryStatus=").append(mBatteryStatus)
299                .append(",recentTemps=");
300        for (int i = 0; i < mNumTemps; i++) {
301            sb.append(mRecentTemps[i]).append(',');
302        }
303        Slog.i(TAG, sb.toString());
304    }
305
306    /**
307     * Calculates and logs min, max, and average
308     * {@link HardwarePropertiesManager#DEVICE_TEMPERATURE_SKIN} over the past
309     * {@link #TEMPERATURE_LOGGING_INTERVAL}.
310     */
311    private void logTemperatureStats() {
312        if (mNextLogTime > System.currentTimeMillis() && mNumTemps != MAX_RECENT_TEMPS) {
313            return;
314        }
315
316        if (mNumTemps > 0) {
317            float sum = mRecentTemps[0], min = mRecentTemps[0], max = mRecentTemps[0];
318            for (int i = 1; i < mNumTemps; i++) {
319                float temp = mRecentTemps[i];
320                sum += temp;
321                if (temp > max) {
322                    max = temp;
323                }
324                if (temp < min) {
325                    min = temp;
326                }
327            }
328
329            float avg = sum / mNumTemps;
330            Slog.i(TAG, "avg=" + avg + ",min=" + min + ",max=" + max);
331            MetricsLogger.histogram(mContext, "device_skin_temp_avg", (int) avg);
332            MetricsLogger.histogram(mContext, "device_skin_temp_min", (int) min);
333            MetricsLogger.histogram(mContext, "device_skin_temp_max", (int) max);
334        }
335        setNextLogTime();
336        mNumTemps = 0;
337    }
338
339    private void setNextLogTime() {
340        mNextLogTime = System.currentTimeMillis() + TEMPERATURE_LOGGING_INTERVAL;
341    }
342
343    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
344        pw.print("mLowBatteryAlertCloseLevel=");
345        pw.println(mLowBatteryAlertCloseLevel);
346        pw.print("mLowBatteryReminderLevels=");
347        pw.println(Arrays.toString(mLowBatteryReminderLevels));
348        pw.print("mBatteryLevel=");
349        pw.println(Integer.toString(mBatteryLevel));
350        pw.print("mBatteryStatus=");
351        pw.println(Integer.toString(mBatteryStatus));
352        pw.print("mPlugType=");
353        pw.println(Integer.toString(mPlugType));
354        pw.print("mInvalidCharger=");
355        pw.println(Integer.toString(mInvalidCharger));
356        pw.print("mScreenOffTime=");
357        pw.print(mScreenOffTime);
358        if (mScreenOffTime >= 0) {
359            pw.print(" (");
360            pw.print(SystemClock.elapsedRealtime() - mScreenOffTime);
361            pw.print(" ago)");
362        }
363        pw.println();
364        pw.print("soundTimeout=");
365        pw.println(Settings.Global.getInt(mContext.getContentResolver(),
366                Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0));
367        pw.print("bucket: ");
368        pw.println(Integer.toString(findBatteryLevelBucket(mBatteryLevel)));
369        pw.print("mThresholdTemp=");
370        pw.println(Float.toString(mThresholdTemp));
371        pw.print("mNextLogTime=");
372        pw.println(Long.toString(mNextLogTime));
373        mWarnings.dump(pw);
374    }
375
376    public interface WarningsUI {
377        void update(int batteryLevel, int bucket, long screenOffTime);
378        void dismissLowBatteryWarning();
379        void showLowBatteryWarning(boolean playSound);
380        void dismissInvalidChargerWarning();
381        void showInvalidChargerWarning();
382        void updateLowBatteryWarning();
383        boolean isInvalidChargerWarningShowing();
384        void dismissHighTemperatureWarning();
385        void showHighTemperatureWarning();
386        void showThermalShutdownWarning();
387        void dump(PrintWriter pw);
388        void userSwitched();
389    }
390}
391
392