• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.systemui.power;
18 
19 import android.app.Notification;
20 import android.app.NotificationManager;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.DialogInterface.OnClickListener;
27 import android.content.DialogInterface.OnDismissListener;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.media.AudioAttributes;
31 import android.net.Uri;
32 import android.os.AsyncTask;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.PowerManager;
36 import android.os.SystemClock;
37 import android.os.UserHandle;
38 import android.provider.Settings;
39 import android.support.annotation.VisibleForTesting;
40 import android.util.Slog;
41 
42 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
43 import com.android.settingslib.Utils;
44 import com.android.systemui.R;
45 import com.android.systemui.SystemUI;
46 import com.android.systemui.statusbar.phone.StatusBar;
47 import com.android.systemui.statusbar.phone.SystemUIDialog;
48 import com.android.systemui.util.NotificationChannels;
49 
50 import java.io.PrintWriter;
51 import java.text.NumberFormat;
52 
53 public class PowerNotificationWarnings implements PowerUI.WarningsUI {
54     private static final String TAG = PowerUI.TAG + ".Notification";
55     private static final boolean DEBUG = PowerUI.DEBUG;
56 
57     private static final String TAG_BATTERY = "low_battery";
58     private static final String TAG_TEMPERATURE = "high_temp";
59 
60     private static final int SHOWING_NOTHING = 0;
61     private static final int SHOWING_WARNING = 1;
62     private static final int SHOWING_INVALID_CHARGER = 3;
63     private static final String[] SHOWING_STRINGS = {
64         "SHOWING_NOTHING",
65         "SHOWING_WARNING",
66         "SHOWING_SAVER",
67         "SHOWING_INVALID_CHARGER",
68     };
69 
70     private static final String ACTION_SHOW_BATTERY_SETTINGS = "PNW.batterySettings";
71     private static final String ACTION_START_SAVER = "PNW.startSaver";
72     private static final String ACTION_DISMISSED_WARNING = "PNW.dismissedWarning";
73     private static final String ACTION_CLICKED_TEMP_WARNING = "PNW.clickedTempWarning";
74     private static final String ACTION_DISMISSED_TEMP_WARNING = "PNW.dismissedTempWarning";
75     private static final String ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING =
76             "PNW.clickedThermalShutdownWarning";
77     private static final String ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING =
78             "PNW.dismissedThermalShutdownWarning";
79 
80     private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
81             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
82             .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
83             .build();
84 
85     private final Context mContext;
86     private final NotificationManager mNoMan;
87     private final PowerManager mPowerMan;
88     private final Handler mHandler = new Handler(Looper.getMainLooper());
89     private final Receiver mReceiver = new Receiver();
90     private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY);
91 
92     private int mBatteryLevel;
93     private int mBucket;
94     private long mScreenOffTime;
95     private int mShowing;
96 
97     private long mBucketDroppedNegativeTimeMs;
98 
99     private boolean mWarning;
100     private boolean mPlaySound;
101     private boolean mInvalidCharger;
102     private SystemUIDialog mSaverConfirmation;
103     private boolean mHighTempWarning;
104     private SystemUIDialog mHighTempDialog;
105     private SystemUIDialog mThermalShutdownDialog;
106 
PowerNotificationWarnings(Context context, NotificationManager notificationManager, StatusBar statusBar)107     public PowerNotificationWarnings(Context context, NotificationManager notificationManager,
108             StatusBar statusBar) {
109         mContext = context;
110         mNoMan = notificationManager;
111         mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
112         mReceiver.init();
113     }
114 
115     @Override
dump(PrintWriter pw)116     public void dump(PrintWriter pw) {
117         pw.print("mWarning="); pw.println(mWarning);
118         pw.print("mPlaySound="); pw.println(mPlaySound);
119         pw.print("mInvalidCharger="); pw.println(mInvalidCharger);
120         pw.print("mShowing="); pw.println(SHOWING_STRINGS[mShowing]);
121         pw.print("mSaverConfirmation="); pw.println(mSaverConfirmation != null ? "not null" : null);
122         pw.print("mHighTempWarning="); pw.println(mHighTempWarning);
123         pw.print("mHighTempDialog="); pw.println(mHighTempDialog != null ? "not null" : null);
124         pw.print("mThermalShutdownDialog=");
125         pw.println(mThermalShutdownDialog != null ? "not null" : null);
126     }
127 
128     @Override
update(int batteryLevel, int bucket, long screenOffTime)129     public void update(int batteryLevel, int bucket, long screenOffTime) {
130         mBatteryLevel = batteryLevel;
131         if (bucket >= 0) {
132             mBucketDroppedNegativeTimeMs = 0;
133         } else if (bucket < mBucket) {
134             mBucketDroppedNegativeTimeMs = System.currentTimeMillis();
135         }
136         mBucket = bucket;
137         mScreenOffTime = screenOffTime;
138     }
139 
updateNotification()140     private void updateNotification() {
141         if (DEBUG) Slog.d(TAG, "updateNotification mWarning=" + mWarning + " mPlaySound="
142                 + mPlaySound + " mInvalidCharger=" + mInvalidCharger);
143         if (mInvalidCharger) {
144             showInvalidChargerNotification();
145             mShowing = SHOWING_INVALID_CHARGER;
146         } else if (mWarning) {
147             showWarningNotification();
148             mShowing = SHOWING_WARNING;
149         } else {
150             mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL);
151             mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL);
152             mShowing = SHOWING_NOTHING;
153         }
154     }
155 
showInvalidChargerNotification()156     private void showInvalidChargerNotification() {
157         final Notification.Builder nb =
158                 new Notification.Builder(mContext, NotificationChannels.ALERTS)
159                         .setSmallIcon(R.drawable.ic_power_low)
160                         .setWhen(0)
161                         .setShowWhen(false)
162                         .setOngoing(true)
163                         .setContentTitle(mContext.getString(R.string.invalid_charger_title))
164                         .setContentText(mContext.getString(R.string.invalid_charger_text))
165                         .setColor(mContext.getColor(
166                                 com.android.internal.R.color.system_notification_accent_color));
167         SystemUI.overrideNotificationAppName(mContext, nb);
168         final Notification n = nb.build();
169         mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL);
170         mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, n, UserHandle.ALL);
171     }
172 
showWarningNotification()173     private void showWarningNotification() {
174         final int textRes = R.string.battery_low_percent_format;
175         final String percentage = NumberFormat.getPercentInstance().format((double) mBatteryLevel / 100.0);
176         final Notification.Builder nb =
177                 new Notification.Builder(mContext, NotificationChannels.ALERTS)
178                         .setSmallIcon(R.drawable.ic_power_low)
179                         // Bump the notification when the bucket dropped.
180                         .setWhen(mBucketDroppedNegativeTimeMs)
181                         .setShowWhen(false)
182                         .setContentTitle(mContext.getString(R.string.battery_low_title))
183                         .setContentText(mContext.getString(textRes, percentage))
184                         .setOnlyAlertOnce(true)
185                         .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING))
186                         .setVisibility(Notification.VISIBILITY_PUBLIC)
187                         .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError));
188         if (hasBatterySettings()) {
189             nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS));
190         }
191         nb.addAction(0,
192                 mContext.getString(R.string.battery_saver_start_action),
193                 pendingBroadcast(ACTION_START_SAVER));
194         if (mPlaySound) {
195             attachLowBatterySound(nb);
196             mPlaySound = false;
197         }
198         SystemUI.overrideNotificationAppName(mContext, nb);
199         final Notification n = nb.build();
200         mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL);
201         mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL);
202     }
203 
pendingBroadcast(String action)204     private PendingIntent pendingBroadcast(String action) {
205         return PendingIntent.getBroadcastAsUser(mContext,
206                 0, new Intent(action), 0, UserHandle.CURRENT);
207     }
208 
settings(String action)209     private static Intent settings(String action) {
210         return new Intent(action).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
211                 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
212                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
213                 | Intent.FLAG_ACTIVITY_NO_HISTORY
214                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
215     }
216 
217     @Override
isInvalidChargerWarningShowing()218     public boolean isInvalidChargerWarningShowing() {
219         return mInvalidCharger;
220     }
221 
222     @Override
dismissHighTemperatureWarning()223     public void dismissHighTemperatureWarning() {
224         if (!mHighTempWarning) {
225             return;
226         }
227         mHighTempWarning = false;
228         dismissHighTemperatureWarningInternal();
229     }
230 
231     /**
232      * Internal only version of {@link #dismissHighTemperatureWarning()} that simply dismisses
233      * the notification. As such, the notification will not show again until
234      * {@link #dismissHighTemperatureWarning()} is called.
235      */
dismissHighTemperatureWarningInternal()236     private void dismissHighTemperatureWarningInternal() {
237         mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, UserHandle.ALL);
238     }
239 
240     @Override
showHighTemperatureWarning()241     public void showHighTemperatureWarning() {
242         if (mHighTempWarning) {
243             return;
244         }
245         mHighTempWarning = true;
246         final Notification.Builder nb =
247                 new Notification.Builder(mContext, NotificationChannels.ALERTS)
248                         .setSmallIcon(R.drawable.ic_device_thermostat_24)
249                         .setWhen(0)
250                         .setShowWhen(false)
251                         .setContentTitle(mContext.getString(R.string.high_temp_title))
252                         .setContentText(mContext.getString(R.string.high_temp_notif_message))
253                         .setVisibility(Notification.VISIBILITY_PUBLIC)
254                         .setContentIntent(pendingBroadcast(ACTION_CLICKED_TEMP_WARNING))
255                         .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_TEMP_WARNING))
256                         .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError));
257         SystemUI.overrideNotificationAppName(mContext, nb);
258         final Notification n = nb.build();
259         mNoMan.notifyAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, n, UserHandle.ALL);
260     }
261 
showHighTemperatureDialog()262     private void showHighTemperatureDialog() {
263         if (mHighTempDialog != null) return;
264         final SystemUIDialog d = new SystemUIDialog(mContext);
265         d.setIconAttribute(android.R.attr.alertDialogIcon);
266         d.setTitle(R.string.high_temp_title);
267         d.setMessage(R.string.high_temp_dialog_message);
268         d.setPositiveButton(com.android.internal.R.string.ok, null);
269         d.setShowForAllUsers(true);
270         d.setOnDismissListener(dialog -> mHighTempDialog = null);
271         d.show();
272         mHighTempDialog = d;
273     }
274 
275     @VisibleForTesting
dismissThermalShutdownWarning()276     void dismissThermalShutdownWarning() {
277         mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, UserHandle.ALL);
278     }
279 
showThermalShutdownDialog()280     private void showThermalShutdownDialog() {
281         if (mThermalShutdownDialog != null) return;
282         final SystemUIDialog d = new SystemUIDialog(mContext);
283         d.setIconAttribute(android.R.attr.alertDialogIcon);
284         d.setTitle(R.string.thermal_shutdown_title);
285         d.setMessage(R.string.thermal_shutdown_dialog_message);
286         d.setPositiveButton(com.android.internal.R.string.ok, null);
287         d.setShowForAllUsers(true);
288         d.setOnDismissListener(dialog -> mThermalShutdownDialog = null);
289         d.show();
290         mThermalShutdownDialog = d;
291     }
292 
293     @Override
showThermalShutdownWarning()294     public void showThermalShutdownWarning() {
295         final Notification.Builder nb =
296                 new Notification.Builder(mContext, NotificationChannels.ALERTS)
297                         .setSmallIcon(R.drawable.ic_device_thermostat_24)
298                         .setWhen(0)
299                         .setShowWhen(false)
300                         .setContentTitle(mContext.getString(R.string.thermal_shutdown_title))
301                         .setContentText(mContext.getString(R.string.thermal_shutdown_message))
302                         .setVisibility(Notification.VISIBILITY_PUBLIC)
303                         .setContentIntent(pendingBroadcast(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING))
304                         .setDeleteIntent(
305                                 pendingBroadcast(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING))
306                         .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError));
307         SystemUI.overrideNotificationAppName(mContext, nb);
308         final Notification n = nb.build();
309         mNoMan.notifyAsUser(
310                 TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, n, UserHandle.ALL);
311     }
312 
313     @Override
updateLowBatteryWarning()314     public void updateLowBatteryWarning() {
315         updateNotification();
316     }
317 
318     @Override
dismissLowBatteryWarning()319     public void dismissLowBatteryWarning() {
320         if (DEBUG) Slog.d(TAG, "dismissing low battery warning: level=" + mBatteryLevel);
321         dismissLowBatteryNotification();
322     }
323 
dismissLowBatteryNotification()324     private void dismissLowBatteryNotification() {
325         if (mWarning) Slog.i(TAG, "dismissing low battery notification");
326         mWarning = false;
327         updateNotification();
328     }
329 
hasBatterySettings()330     private boolean hasBatterySettings() {
331         return mOpenBatterySettings.resolveActivity(mContext.getPackageManager()) != null;
332     }
333 
334     @Override
showLowBatteryWarning(boolean playSound)335     public void showLowBatteryWarning(boolean playSound) {
336         Slog.i(TAG,
337                 "show low battery warning: level=" + mBatteryLevel
338                 + " [" + mBucket + "] playSound=" + playSound);
339         mPlaySound = playSound;
340         mWarning = true;
341         updateNotification();
342     }
343 
attachLowBatterySound(Notification.Builder b)344     private void attachLowBatterySound(Notification.Builder b) {
345         final ContentResolver cr = mContext.getContentResolver();
346 
347         final int silenceAfter = Settings.Global.getInt(cr,
348                 Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0);
349         final long offTime = SystemClock.elapsedRealtime() - mScreenOffTime;
350         if (silenceAfter > 0
351                 && mScreenOffTime > 0
352                 && offTime > silenceAfter) {
353             Slog.i(TAG, "screen off too long (" + offTime + "ms, limit " + silenceAfter
354                     + "ms): not waking up the user with low battery sound");
355             return;
356         }
357 
358         if (DEBUG) {
359             Slog.d(TAG, "playing low battery sound. pick-a-doop!"); // WOMP-WOMP is deprecated
360         }
361 
362         if (Settings.Global.getInt(cr, Settings.Global.POWER_SOUNDS_ENABLED, 1) == 1) {
363             final String soundPath = Settings.Global.getString(cr,
364                     Settings.Global.LOW_BATTERY_SOUND);
365             if (soundPath != null) {
366                 final Uri soundUri = Uri.parse("file://" + soundPath);
367                 if (soundUri != null) {
368                     b.setSound(soundUri, AUDIO_ATTRIBUTES);
369                     if (DEBUG) Slog.d(TAG, "playing sound " + soundUri);
370                 }
371             }
372         }
373     }
374 
375     @Override
dismissInvalidChargerWarning()376     public void dismissInvalidChargerWarning() {
377         dismissInvalidChargerNotification();
378     }
379 
dismissInvalidChargerNotification()380     private void dismissInvalidChargerNotification() {
381         if (mInvalidCharger) Slog.i(TAG, "dismissing invalid charger notification");
382         mInvalidCharger = false;
383         updateNotification();
384     }
385 
386     @Override
showInvalidChargerWarning()387     public void showInvalidChargerWarning() {
388         mInvalidCharger = true;
389         updateNotification();
390     }
391 
392     @Override
userSwitched()393     public void userSwitched() {
394         updateNotification();
395     }
396 
showStartSaverConfirmation()397     private void showStartSaverConfirmation() {
398         if (mSaverConfirmation != null) return;
399         final SystemUIDialog d = new SystemUIDialog(mContext);
400         d.setTitle(R.string.battery_saver_confirmation_title);
401         d.setMessage(com.android.internal.R.string.battery_saver_description);
402         d.setNegativeButton(android.R.string.cancel, null);
403         d.setPositiveButton(R.string.battery_saver_confirmation_ok, mStartSaverMode);
404         d.setShowForAllUsers(true);
405         d.setOnDismissListener(new OnDismissListener() {
406             @Override
407             public void onDismiss(DialogInterface dialog) {
408                 mSaverConfirmation = null;
409             }
410         });
411         d.show();
412         mSaverConfirmation = d;
413     }
414 
setSaverMode(boolean mode)415     private void setSaverMode(boolean mode) {
416         mPowerMan.setPowerSaveMode(mode);
417     }
418 
419     private final class Receiver extends BroadcastReceiver {
420 
init()421         public void init() {
422             IntentFilter filter = new IntentFilter();
423             filter.addAction(ACTION_SHOW_BATTERY_SETTINGS);
424             filter.addAction(ACTION_START_SAVER);
425             filter.addAction(ACTION_DISMISSED_WARNING);
426             filter.addAction(ACTION_CLICKED_TEMP_WARNING);
427             filter.addAction(ACTION_DISMISSED_TEMP_WARNING);
428             filter.addAction(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING);
429             filter.addAction(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING);
430             mContext.registerReceiverAsUser(this, UserHandle.ALL, filter,
431                     android.Manifest.permission.STATUS_BAR_SERVICE, mHandler);
432         }
433 
434         @Override
onReceive(Context context, Intent intent)435         public void onReceive(Context context, Intent intent) {
436             final String action = intent.getAction();
437             Slog.i(TAG, "Received " + action);
438             if (action.equals(ACTION_SHOW_BATTERY_SETTINGS)) {
439                 dismissLowBatteryNotification();
440                 mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT);
441             } else if (action.equals(ACTION_START_SAVER)) {
442                 dismissLowBatteryNotification();
443                 showStartSaverConfirmation();
444             } else if (action.equals(ACTION_DISMISSED_WARNING)) {
445                 dismissLowBatteryWarning();
446             } else if (ACTION_CLICKED_TEMP_WARNING.equals(action)) {
447                 dismissHighTemperatureWarningInternal();
448                 showHighTemperatureDialog();
449             } else if (ACTION_DISMISSED_TEMP_WARNING.equals(action)) {
450                 dismissHighTemperatureWarningInternal();
451             } else if (ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING.equals(action)) {
452                 dismissThermalShutdownWarning();
453                 showThermalShutdownDialog();
454             } else if (ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING.equals(action)) {
455                 dismissThermalShutdownWarning();
456             }
457         }
458     }
459 
460     private final OnClickListener mStartSaverMode = new OnClickListener() {
461         @Override
462         public void onClick(DialogInterface dialog, int which) {
463             AsyncTask.execute(new Runnable() {
464                 @Override
465                 public void run() {
466                     setSaverMode(true);
467                 }
468             });
469         }
470     };
471 }
472