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