• 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 static android.app.PendingIntent.FLAG_IMMUTABLE;
20 
21 import android.app.KeyguardManager;
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.content.ActivityNotFoundException;
26 import android.content.BroadcastReceiver;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.DialogInterface;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.media.AudioAttributes;
33 import android.net.Uri;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Looper;
37 import android.os.PowerManager;
38 import android.os.UserHandle;
39 import android.provider.Settings;
40 import android.provider.Settings.Global;
41 import android.provider.Settings.Secure;
42 import android.text.Annotation;
43 import android.text.Layout;
44 import android.text.SpannableString;
45 import android.text.SpannableStringBuilder;
46 import android.text.TextPaint;
47 import android.text.TextUtils;
48 import android.text.method.LinkMovementMethod;
49 import android.text.style.URLSpan;
50 import android.util.Log;
51 import android.util.Slog;
52 import android.view.View;
53 import android.view.WindowManager;
54 
55 import androidx.annotation.VisibleForTesting;
56 
57 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
58 import com.android.settingslib.Utils;
59 import com.android.settingslib.fuelgauge.BatterySaverUtils;
60 import com.android.settingslib.utils.PowerUtil;
61 import com.android.systemui.Dependency;
62 import com.android.systemui.R;
63 import com.android.systemui.SystemUI;
64 import com.android.systemui.dagger.SysUISingleton;
65 import com.android.systemui.plugins.ActivityStarter;
66 import com.android.systemui.statusbar.phone.SystemUIDialog;
67 import com.android.systemui.util.NotificationChannels;
68 import com.android.systemui.volume.Events;
69 
70 import java.io.PrintWriter;
71 import java.text.NumberFormat;
72 import java.util.Locale;
73 import java.util.Objects;
74 
75 import javax.inject.Inject;
76 
77 /**
78  */
79 @SysUISingleton
80 public class PowerNotificationWarnings implements PowerUI.WarningsUI {
81 
82     private static final String TAG = PowerUI.TAG + ".Notification";
83     private static final boolean DEBUG = PowerUI.DEBUG;
84 
85     private static final String TAG_BATTERY = "low_battery";
86     private static final String TAG_TEMPERATURE = "high_temp";
87     private static final String TAG_AUTO_SAVER = "auto_saver";
88 
89     private static final int SHOWING_NOTHING = 0;
90     private static final int SHOWING_WARNING = 1;
91     private static final int SHOWING_INVALID_CHARGER = 3;
92     private static final int SHOWING_AUTO_SAVER_SUGGESTION = 4;
93     private static final String[] SHOWING_STRINGS = {
94         "SHOWING_NOTHING",
95         "SHOWING_WARNING",
96         "SHOWING_SAVER",
97         "SHOWING_INVALID_CHARGER",
98         "SHOWING_AUTO_SAVER_SUGGESTION",
99     };
100 
101     private static final String ACTION_SHOW_BATTERY_SETTINGS = "PNW.batterySettings";
102     private static final String ACTION_START_SAVER = "PNW.startSaver";
103     private static final String ACTION_DISMISSED_WARNING = "PNW.dismissedWarning";
104     private static final String ACTION_CLICKED_TEMP_WARNING = "PNW.clickedTempWarning";
105     private static final String ACTION_DISMISSED_TEMP_WARNING = "PNW.dismissedTempWarning";
106     private static final String ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING =
107             "PNW.clickedThermalShutdownWarning";
108     private static final String ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING =
109             "PNW.dismissedThermalShutdownWarning";
110     private static final String ACTION_SHOW_START_SAVER_CONFIRMATION =
111             BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION;
112     private static final String ACTION_SHOW_AUTO_SAVER_SUGGESTION =
113             BatterySaverUtils.ACTION_SHOW_AUTO_SAVER_SUGGESTION;
114     private static final String ACTION_DISMISS_AUTO_SAVER_SUGGESTION =
115             "PNW.dismissAutoSaverSuggestion";
116 
117     private static final String ACTION_ENABLE_AUTO_SAVER =
118             "PNW.enableAutoSaver";
119     private static final String ACTION_AUTO_SAVER_NO_THANKS =
120             "PNW.autoSaverNoThanks";
121 
122     private static final String SETTINGS_ACTION_OPEN_BATTERY_SAVER_SETTING =
123             "android.settings.BATTERY_SAVER_SETTINGS";
124     public static final String BATTERY_SAVER_SCHEDULE_SCREEN_INTENT_ACTION =
125             "com.android.settings.BATTERY_SAVER_SCHEDULE_SETTINGS";
126 
127     private static final String BATTERY_SAVER_DESCRIPTION_URL_KEY = "url";
128 
129     private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
130             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
131             .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
132             .build();
133     public static final String EXTRA_CONFIRM_ONLY = "extra_confirm_only";
134 
135     private final Context mContext;
136     private final NotificationManager mNoMan;
137     private final PowerManager mPowerMan;
138     private final KeyguardManager mKeyguard;
139     private final Handler mHandler = new Handler(Looper.getMainLooper());
140     private final Receiver mReceiver = new Receiver();
141     private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY);
142 
143     private int mBatteryLevel;
144     private int mBucket;
145     private long mScreenOffTime;
146     private int mShowing;
147 
148     private long mWarningTriggerTimeMs;
149     private boolean mWarning;
150     private boolean mShowAutoSaverSuggestion;
151     private boolean mPlaySound;
152     private boolean mInvalidCharger;
153     private SystemUIDialog mSaverConfirmation;
154     private SystemUIDialog mSaverEnabledConfirmation;
155     private boolean mHighTempWarning;
156     private SystemUIDialog mHighTempDialog;
157     private SystemUIDialog mThermalShutdownDialog;
158     @VisibleForTesting SystemUIDialog mUsbHighTempDialog;
159     private BatteryStateSnapshot mCurrentBatterySnapshot;
160     private ActivityStarter mActivityStarter;
161 
162     /**
163      */
164     @Inject
PowerNotificationWarnings(Context context, ActivityStarter activityStarter)165     public PowerNotificationWarnings(Context context, ActivityStarter activityStarter) {
166         mContext = context;
167         mNoMan = mContext.getSystemService(NotificationManager.class);
168         mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
169         mKeyguard = mContext.getSystemService(KeyguardManager.class);
170         mReceiver.init();
171         mActivityStarter = activityStarter;
172     }
173 
174     @Override
dump(PrintWriter pw)175     public void dump(PrintWriter pw) {
176         pw.print("mWarning="); pw.println(mWarning);
177         pw.print("mPlaySound="); pw.println(mPlaySound);
178         pw.print("mInvalidCharger="); pw.println(mInvalidCharger);
179         pw.print("mShowing="); pw.println(SHOWING_STRINGS[mShowing]);
180         pw.print("mSaverConfirmation="); pw.println(mSaverConfirmation != null ? "not null" : null);
181         pw.print("mSaverEnabledConfirmation=");
182         pw.print("mHighTempWarning="); pw.println(mHighTempWarning);
183         pw.print("mHighTempDialog="); pw.println(mHighTempDialog != null ? "not null" : null);
184         pw.print("mThermalShutdownDialog=");
185         pw.println(mThermalShutdownDialog != null ? "not null" : null);
186         pw.print("mUsbHighTempDialog=");
187         pw.println(mUsbHighTempDialog != null ? "not null" : null);
188     }
189 
getLowBatteryAutoTriggerDefaultLevel()190     private int getLowBatteryAutoTriggerDefaultLevel() {
191         return mContext.getResources().getInteger(
192                 com.android.internal.R.integer.config_lowBatteryAutoTriggerDefaultLevel);
193     }
194 
195     @Override
update(int batteryLevel, int bucket, long screenOffTime)196     public void update(int batteryLevel, int bucket, long screenOffTime) {
197         mBatteryLevel = batteryLevel;
198         if (bucket >= 0) {
199             mWarningTriggerTimeMs = 0;
200         } else if (bucket < mBucket) {
201             mWarningTriggerTimeMs = System.currentTimeMillis();
202         }
203         mBucket = bucket;
204         mScreenOffTime = screenOffTime;
205     }
206 
207     @Override
updateSnapshot(BatteryStateSnapshot snapshot)208     public void updateSnapshot(BatteryStateSnapshot snapshot) {
209         mCurrentBatterySnapshot = snapshot;
210     }
211 
updateNotification()212     private void updateNotification() {
213         if (DEBUG) Slog.d(TAG, "updateNotification mWarning=" + mWarning + " mPlaySound="
214                 + mPlaySound + " mInvalidCharger=" + mInvalidCharger);
215         if (mInvalidCharger) {
216             showInvalidChargerNotification();
217             mShowing = SHOWING_INVALID_CHARGER;
218         } else if (mWarning) {
219             showWarningNotification();
220             mShowing = SHOWING_WARNING;
221         } else if (mShowAutoSaverSuggestion) {
222             // Once we showed the notification, don't show it again until it goes SHOWING_NOTHING.
223             // This shouldn't be needed, because we have a delete intent on this notification
224             // so when it's dismissed we should notice it and clear mShowAutoSaverSuggestion,
225             // However we double check here just in case the dismiss intent broadcast is delayed.
226             if (mShowing != SHOWING_AUTO_SAVER_SUGGESTION) {
227                 showAutoSaverSuggestionNotification();
228             }
229             mShowing = SHOWING_AUTO_SAVER_SUGGESTION;
230         } else {
231             mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL);
232             mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL);
233             mNoMan.cancelAsUser(TAG_AUTO_SAVER,
234                     SystemMessage.NOTE_AUTO_SAVER_SUGGESTION, UserHandle.ALL);
235             mShowing = SHOWING_NOTHING;
236         }
237     }
238 
showInvalidChargerNotification()239     private void showInvalidChargerNotification() {
240         final Notification.Builder nb =
241                 new Notification.Builder(mContext, NotificationChannels.ALERTS)
242                         .setSmallIcon(R.drawable.ic_power_low)
243                         .setWhen(0)
244                         .setShowWhen(false)
245                         .setOngoing(true)
246                         .setContentTitle(mContext.getString(R.string.invalid_charger_title))
247                         .setContentText(mContext.getString(R.string.invalid_charger_text))
248                         .setColor(mContext.getColor(
249                                 com.android.internal.R.color.system_notification_accent_color));
250         SystemUI.overrideNotificationAppName(mContext, nb, false);
251         final Notification n = nb.build();
252         mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL);
253         mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, n, UserHandle.ALL);
254     }
255 
showWarningNotification()256     protected void showWarningNotification() {
257         final String percentage = NumberFormat.getPercentInstance()
258                 .format((double) mCurrentBatterySnapshot.getBatteryLevel() / 100.0);
259 
260         // get shared standard notification copy
261         String title = mContext.getString(R.string.battery_low_title);
262         String contentText;
263 
264         // get correct content text if notification is hybrid or not
265         if (mCurrentBatterySnapshot.isHybrid()) {
266             contentText = getHybridContentString(percentage);
267         } else {
268             contentText = mContext.getString(R.string.battery_low_percent_format, percentage);
269         }
270 
271         final Notification.Builder nb =
272                 new Notification.Builder(mContext, NotificationChannels.BATTERY)
273                         .setSmallIcon(R.drawable.ic_power_low)
274                         // Bump the notification when the bucket dropped.
275                         .setWhen(mWarningTriggerTimeMs)
276                         .setShowWhen(false)
277                         .setContentText(contentText)
278                         .setContentTitle(title)
279                         .setOnlyAlertOnce(true)
280                         .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING))
281                         .setStyle(new Notification.BigTextStyle().bigText(contentText))
282                         .setVisibility(Notification.VISIBILITY_PUBLIC);
283         if (hasBatterySettings()) {
284             nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS));
285         }
286         // Make the notification red if the percentage goes below a certain amount or the time
287         // remaining estimate is disabled
288         if (!mCurrentBatterySnapshot.isHybrid() || mBucket < 0
289                 || mCurrentBatterySnapshot.getTimeRemainingMillis()
290                         < mCurrentBatterySnapshot.getSevereThresholdMillis()) {
291             nb.setColor(Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorError));
292         }
293 
294         if (!mPowerMan.isPowerSaveMode()) {
295             nb.addAction(0,
296                     mContext.getString(R.string.battery_saver_start_action),
297                     pendingBroadcast(ACTION_START_SAVER));
298         }
299         nb.setOnlyAlertOnce(!mPlaySound);
300         mPlaySound = false;
301         SystemUI.overrideNotificationAppName(mContext, nb, false);
302         final Notification n = nb.build();
303         mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL);
304         mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL);
305     }
306 
showAutoSaverSuggestionNotification()307     private void showAutoSaverSuggestionNotification() {
308         final CharSequence message = mContext.getString(R.string.auto_saver_text);
309         final Notification.Builder nb =
310                 new Notification.Builder(mContext, NotificationChannels.HINTS)
311                         .setSmallIcon(R.drawable.ic_power_saver)
312                         .setWhen(0)
313                         .setShowWhen(false)
314                         .setContentTitle(mContext.getString(R.string.auto_saver_title))
315                         .setStyle(new Notification.BigTextStyle().bigText(message))
316                         .setContentText(message);
317         nb.setContentIntent(pendingBroadcast(ACTION_ENABLE_AUTO_SAVER));
318         nb.setDeleteIntent(pendingBroadcast(ACTION_DISMISS_AUTO_SAVER_SUGGESTION));
319         nb.addAction(0,
320                 mContext.getString(R.string.no_auto_saver_action),
321                 pendingBroadcast(ACTION_AUTO_SAVER_NO_THANKS));
322 
323         SystemUI.overrideNotificationAppName(mContext, nb, false);
324 
325         final Notification n = nb.build();
326         mNoMan.notifyAsUser(
327                 TAG_AUTO_SAVER, SystemMessage.NOTE_AUTO_SAVER_SUGGESTION, n, UserHandle.ALL);
328     }
329 
getHybridContentString(String percentage)330     private String getHybridContentString(String percentage) {
331         return PowerUtil.getBatteryRemainingStringFormatted(
332                 mContext,
333                 mCurrentBatterySnapshot.getTimeRemainingMillis(),
334                 percentage,
335                 mCurrentBatterySnapshot.isBasedOnUsage());
336     }
337 
pendingBroadcast(String action)338     private PendingIntent pendingBroadcast(String action) {
339         return PendingIntent.getBroadcastAsUser(
340                 mContext,
341                 0 /* request code */,
342                 new Intent(action)
343                         .setPackage(mContext.getPackageName())
344                         .setFlags(Intent.FLAG_RECEIVER_FOREGROUND),
345                 FLAG_IMMUTABLE /* flags */,
346                 UserHandle.CURRENT);
347     }
348 
settings(String action)349     private static Intent settings(String action) {
350         return new Intent(action).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
351                 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
352                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
353                 | Intent.FLAG_ACTIVITY_NO_HISTORY
354                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
355     }
356 
357     @Override
isInvalidChargerWarningShowing()358     public boolean isInvalidChargerWarningShowing() {
359         return mInvalidCharger;
360     }
361 
362     @Override
dismissHighTemperatureWarning()363     public void dismissHighTemperatureWarning() {
364         if (!mHighTempWarning) {
365             return;
366         }
367         dismissHighTemperatureWarningInternal();
368     }
369 
370     /**
371      * Internal only version of {@link #dismissHighTemperatureWarning()} that simply dismisses
372      * the notification. As such, the notification will not show again until
373      * {@link #dismissHighTemperatureWarning()} is called.
374      */
dismissHighTemperatureWarningInternal()375     private void dismissHighTemperatureWarningInternal() {
376         mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, UserHandle.ALL);
377         mHighTempWarning = false;
378     }
379 
380     @Override
showHighTemperatureWarning()381     public void showHighTemperatureWarning() {
382         if (mHighTempWarning) {
383             return;
384         }
385         mHighTempWarning = true;
386         final String message = mContext.getString(R.string.high_temp_notif_message);
387         final Notification.Builder nb =
388                 new Notification.Builder(mContext, NotificationChannels.ALERTS)
389                         .setSmallIcon(R.drawable.ic_device_thermostat_24)
390                         .setWhen(0)
391                         .setShowWhen(false)
392                         .setContentTitle(mContext.getString(R.string.high_temp_title))
393                         .setContentText(message)
394                         .setStyle(new Notification.BigTextStyle().bigText(message))
395                         .setVisibility(Notification.VISIBILITY_PUBLIC)
396                         .setContentIntent(pendingBroadcast(ACTION_CLICKED_TEMP_WARNING))
397                         .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_TEMP_WARNING))
398                         .setColor(Utils.getColorAttrDefaultColor(mContext,
399                                 android.R.attr.colorError));
400         SystemUI.overrideNotificationAppName(mContext, nb, false);
401         final Notification n = nb.build();
402         mNoMan.notifyAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, n, UserHandle.ALL);
403     }
404 
showHighTemperatureDialog()405     private void showHighTemperatureDialog() {
406         if (mHighTempDialog != null) return;
407         final SystemUIDialog d = new SystemUIDialog(mContext);
408         d.setIconAttribute(android.R.attr.alertDialogIcon);
409         d.setTitle(R.string.high_temp_title);
410         d.setMessage(R.string.high_temp_dialog_message);
411         d.setPositiveButton(com.android.internal.R.string.ok, null);
412         d.setShowForAllUsers(true);
413         d.setOnDismissListener(dialog -> mHighTempDialog = null);
414         final String url = mContext.getString(R.string.high_temp_dialog_help_url);
415         if (!url.isEmpty()) {
416             d.setNeutralButton(R.string.high_temp_dialog_help_text,
417                     new DialogInterface.OnClickListener() {
418                         @Override
419                         public void onClick(DialogInterface dialog, int which) {
420                             final Intent helpIntent =
421                                     new Intent(Intent.ACTION_VIEW)
422                                             .setData(Uri.parse(url))
423                                             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
424                             Dependency.get(ActivityStarter.class).startActivity(helpIntent,
425                                     true /* dismissShade */, resultCode -> {
426                                         mHighTempDialog = null;
427                                     });
428                         }
429                     });
430         }
431         d.show();
432         mHighTempDialog = d;
433     }
434 
435     @VisibleForTesting
dismissThermalShutdownWarning()436     void dismissThermalShutdownWarning() {
437         mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, UserHandle.ALL);
438     }
439 
showThermalShutdownDialog()440     private void showThermalShutdownDialog() {
441         if (mThermalShutdownDialog != null) return;
442         final SystemUIDialog d = new SystemUIDialog(mContext);
443         d.setIconAttribute(android.R.attr.alertDialogIcon);
444         d.setTitle(R.string.thermal_shutdown_title);
445         d.setMessage(R.string.thermal_shutdown_dialog_message);
446         d.setPositiveButton(com.android.internal.R.string.ok, null);
447         d.setShowForAllUsers(true);
448         d.setOnDismissListener(dialog -> mThermalShutdownDialog = null);
449         final String url = mContext.getString(R.string.thermal_shutdown_dialog_help_url);
450         if (!url.isEmpty()) {
451             d.setNeutralButton(R.string.thermal_shutdown_dialog_help_text,
452                     new DialogInterface.OnClickListener() {
453                         @Override
454                         public void onClick(DialogInterface dialog, int which) {
455                             final Intent helpIntent =
456                                     new Intent(Intent.ACTION_VIEW)
457                                             .setData(Uri.parse(url))
458                                             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
459                             Dependency.get(ActivityStarter.class).startActivity(helpIntent,
460                                     true /* dismissShade */, resultCode -> {
461                                         mThermalShutdownDialog = null;
462                                     });
463                         }
464                     });
465         }
466         d.show();
467         mThermalShutdownDialog = d;
468     }
469 
470     @Override
showThermalShutdownWarning()471     public void showThermalShutdownWarning() {
472         final String message = mContext.getString(R.string.thermal_shutdown_message);
473         final Notification.Builder nb =
474                 new Notification.Builder(mContext, NotificationChannels.ALERTS)
475                         .setSmallIcon(R.drawable.ic_device_thermostat_24)
476                         .setWhen(0)
477                         .setShowWhen(false)
478                         .setContentTitle(mContext.getString(R.string.thermal_shutdown_title))
479                         .setContentText(message)
480                         .setStyle(new Notification.BigTextStyle().bigText(message))
481                         .setVisibility(Notification.VISIBILITY_PUBLIC)
482                         .setContentIntent(pendingBroadcast(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING))
483                         .setDeleteIntent(
484                                 pendingBroadcast(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING))
485                         .setColor(Utils.getColorAttrDefaultColor(mContext,
486                                 android.R.attr.colorError));
487         SystemUI.overrideNotificationAppName(mContext, nb, false);
488         final Notification n = nb.build();
489         mNoMan.notifyAsUser(
490                 TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, n, UserHandle.ALL);
491     }
492 
493     @Override
showUsbHighTemperatureAlarm()494     public void showUsbHighTemperatureAlarm() {
495         mHandler.post(() -> showUsbHighTemperatureAlarmInternal());
496     }
497 
showUsbHighTemperatureAlarmInternal()498     private void showUsbHighTemperatureAlarmInternal() {
499         if (mUsbHighTempDialog != null) {
500             return;
501         }
502 
503         final SystemUIDialog d = new SystemUIDialog(mContext, R.style.Theme_SystemUI_Dialog_Alert);
504         d.setCancelable(false);
505         d.setIconAttribute(android.R.attr.alertDialogIcon);
506         d.setTitle(R.string.high_temp_alarm_title);
507         d.setShowForAllUsers(true);
508         d.setMessage(mContext.getString(R.string.high_temp_alarm_notify_message, ""));
509         d.setPositiveButton((com.android.internal.R.string.ok),
510                 (dialogInterface, which) -> mUsbHighTempDialog = null);
511         d.setNegativeButton((R.string.high_temp_alarm_help_care_steps),
512                 (dialogInterface, which) -> {
513                     final String contextString = mContext.getString(
514                             R.string.high_temp_alarm_help_url);
515                     final Intent helpIntent = new Intent();
516                     helpIntent.setClassName("com.android.settings",
517                             "com.android.settings.HelpTrampoline");
518                     helpIntent.putExtra(Intent.EXTRA_TEXT, contextString);
519                     Dependency.get(ActivityStarter.class).startActivity(helpIntent,
520                             true /* dismissShade */, resultCode -> {
521                                 mUsbHighTempDialog = null;
522                             });
523                 });
524         d.setOnDismissListener(dialogInterface -> {
525             mUsbHighTempDialog = null;
526             Events.writeEvent(Events.EVENT_DISMISS_USB_OVERHEAT_ALARM,
527                     Events.DISMISS_REASON_USB_OVERHEAD_ALARM_CHANGED,
528                     mKeyguard.isKeyguardLocked());
529         });
530         d.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
531                 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
532         d.show();
533         mUsbHighTempDialog = d;
534 
535         Events.writeEvent(Events.EVENT_SHOW_USB_OVERHEAT_ALARM,
536                 Events.SHOW_REASON_USB_OVERHEAD_ALARM_CHANGED,
537                 mKeyguard.isKeyguardLocked());
538     }
539 
540     @Override
updateLowBatteryWarning()541     public void updateLowBatteryWarning() {
542         updateNotification();
543     }
544 
545     @Override
dismissLowBatteryWarning()546     public void dismissLowBatteryWarning() {
547         if (DEBUG) Slog.d(TAG, "dismissing low battery warning: level=" + mBatteryLevel);
548         dismissLowBatteryNotification();
549     }
550 
dismissLowBatteryNotification()551     private void dismissLowBatteryNotification() {
552         if (mWarning) Slog.i(TAG, "dismissing low battery notification");
553         mWarning = false;
554         updateNotification();
555     }
556 
hasBatterySettings()557     private boolean hasBatterySettings() {
558         return mOpenBatterySettings.resolveActivity(mContext.getPackageManager()) != null;
559     }
560 
561     @Override
showLowBatteryWarning(boolean playSound)562     public void showLowBatteryWarning(boolean playSound) {
563         Slog.i(TAG,
564                 "show low battery warning: level=" + mBatteryLevel
565                         + " [" + mBucket + "] playSound=" + playSound);
566         mPlaySound = playSound;
567         mWarning = true;
568         updateNotification();
569     }
570 
571     @Override
dismissInvalidChargerWarning()572     public void dismissInvalidChargerWarning() {
573         dismissInvalidChargerNotification();
574     }
575 
dismissInvalidChargerNotification()576     private void dismissInvalidChargerNotification() {
577         if (mInvalidCharger) Slog.i(TAG, "dismissing invalid charger notification");
578         mInvalidCharger = false;
579         updateNotification();
580     }
581 
582     @Override
showInvalidChargerWarning()583     public void showInvalidChargerWarning() {
584         mInvalidCharger = true;
585         updateNotification();
586     }
587 
showAutoSaverSuggestion()588     private void showAutoSaverSuggestion() {
589         mShowAutoSaverSuggestion = true;
590         updateNotification();
591     }
592 
dismissAutoSaverSuggestion()593     private void dismissAutoSaverSuggestion() {
594         mShowAutoSaverSuggestion = false;
595         updateNotification();
596     }
597 
598     @Override
userSwitched()599     public void userSwitched() {
600         updateNotification();
601     }
602 
showStartSaverConfirmation(Bundle extras)603     private void showStartSaverConfirmation(Bundle extras) {
604         if (mSaverConfirmation != null) return;
605         final SystemUIDialog d = new SystemUIDialog(mContext);
606         final boolean confirmOnly = extras.getBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY);
607         final int batterySaverTriggerMode =
608                 extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER,
609                         PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
610         final int batterySaverTriggerLevel =
611                 extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL, 0);
612         d.setMessage(getBatterySaverDescription());
613 
614         // Sad hack for http://b/78261259 and http://b/78298335. Otherwise "Battery" may be split
615         // into "Bat-tery".
616         if (isEnglishLocale()) {
617             d.setMessageHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
618         }
619         // We need to set LinkMovementMethod to make the link clickable.
620         d.setMessageMovementMethod(LinkMovementMethod.getInstance());
621 
622         if (confirmOnly) {
623             d.setTitle(R.string.battery_saver_confirmation_title_generic);
624             d.setPositiveButton(com.android.internal.R.string.confirm_battery_saver,
625                     (dialog, which) -> {
626                         final ContentResolver resolver = mContext.getContentResolver();
627                         Settings.Global.putInt(
628                                 resolver,
629                                 Global.AUTOMATIC_POWER_SAVE_MODE,
630                                 batterySaverTriggerMode);
631                         Settings.Global.putInt(
632                                 resolver,
633                                 Global.LOW_POWER_MODE_TRIGGER_LEVEL,
634                                 batterySaverTriggerLevel);
635                         Secure.putIntForUser(
636                                 resolver,
637                                 Secure.LOW_POWER_WARNING_ACKNOWLEDGED,
638                                 1, UserHandle.USER_CURRENT);
639                     });
640         } else {
641             d.setTitle(R.string.battery_saver_confirmation_title);
642             d.setPositiveButton(R.string.battery_saver_confirmation_ok,
643                     (dialog, which) -> setSaverMode(true, false));
644             d.setNegativeButton(android.R.string.cancel, null);
645         }
646         d.setShowForAllUsers(true);
647         d.setOnDismissListener((dialog) -> mSaverConfirmation = null);
648         d.show();
649         mSaverConfirmation = d;
650     }
651 
isEnglishLocale()652     private boolean isEnglishLocale() {
653         return Objects.equals(Locale.getDefault().getLanguage(),
654                 Locale.ENGLISH.getLanguage());
655     }
656 
657     /**
658      * Generates the message for the "want to start battery saver?" dialog with a "learn more" link.
659      */
getBatterySaverDescription()660     private CharSequence getBatterySaverDescription() {
661         final String learnMoreUrl = mContext.getText(
662                 R.string.help_uri_battery_saver_learn_more_link_target).toString();
663 
664         // If there's no link, use the string with no "learn more".
665         if (TextUtils.isEmpty(learnMoreUrl)) {
666             return mContext.getText(
667                     com.android.internal.R.string.battery_saver_description);
668         }
669 
670         // If we have a link, use the string with the "learn more" link.
671         final CharSequence rawText = mContext.getText(
672                 com.android.internal.R.string.battery_saver_description_with_learn_more);
673         final SpannableString message = new SpannableString(rawText);
674         final SpannableStringBuilder builder = new SpannableStringBuilder(message);
675 
676         // Look for the "learn more" part of the string, and set a URL span on it.
677         // We use a customized URLSpan to add FLAG_RECEIVER_FOREGROUND to the intent, and
678         // also to close the dialog.
679         for (Annotation annotation : message.getSpans(0, message.length(), Annotation.class)) {
680             final String key = annotation.getValue();
681 
682             if (!BATTERY_SAVER_DESCRIPTION_URL_KEY.equals(key)) {
683                 continue;
684             }
685             final int start = message.getSpanStart(annotation);
686             final int end = message.getSpanEnd(annotation);
687 
688             // Replace the "learn more" with a custom URL span, with
689             // - No underline.
690             // - When clicked, close the dialog and the notification shade.
691             final URLSpan urlSpan = new URLSpan(learnMoreUrl) {
692                 @Override
693                 public void updateDrawState(TextPaint ds) {
694                     super.updateDrawState(ds);
695                     ds.setUnderlineText(false);
696                 }
697 
698                 @Override
699                 public void onClick(View widget) {
700                     // Close the parent dialog.
701                     if (mSaverConfirmation != null) {
702                         mSaverConfirmation.dismiss();
703                     }
704                     // Also close the notification shade, if it's open.
705                     mContext.sendBroadcast(
706                             new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
707                             .setFlags(Intent.FLAG_RECEIVER_FOREGROUND));
708 
709                     final Uri uri = Uri.parse(getURL());
710                     Context context = widget.getContext();
711                     Intent intent = new Intent(Intent.ACTION_VIEW, uri)
712                             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
713                     try {
714                         context.startActivity(intent);
715                     } catch (ActivityNotFoundException e) {
716                         Log.w(TAG, "Activity was not found for intent, " + intent.toString());
717                     }
718                 }
719             };
720             builder.setSpan(urlSpan, start, end, message.getSpanFlags(urlSpan));
721         }
722         return builder;
723     }
724 
setSaverMode(boolean mode, boolean needFirstTimeWarning)725     private void setSaverMode(boolean mode, boolean needFirstTimeWarning) {
726         BatterySaverUtils.setPowerSaveMode(mContext, mode, needFirstTimeWarning);
727     }
728 
startBatterySaverSchedulePage()729     private void startBatterySaverSchedulePage() {
730         Intent intent = new Intent(BATTERY_SAVER_SCHEDULE_SCREEN_INTENT_ACTION);
731         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
732         mActivityStarter.startActivity(intent, true /* dismissShade */);
733     }
734 
735     private final class Receiver extends BroadcastReceiver {
736 
init()737         public void init() {
738             IntentFilter filter = new IntentFilter();
739             filter.addAction(ACTION_SHOW_BATTERY_SETTINGS);
740             filter.addAction(ACTION_START_SAVER);
741             filter.addAction(ACTION_DISMISSED_WARNING);
742             filter.addAction(ACTION_CLICKED_TEMP_WARNING);
743             filter.addAction(ACTION_DISMISSED_TEMP_WARNING);
744             filter.addAction(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING);
745             filter.addAction(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING);
746             filter.addAction(ACTION_SHOW_START_SAVER_CONFIRMATION);
747             filter.addAction(ACTION_SHOW_AUTO_SAVER_SUGGESTION);
748             filter.addAction(ACTION_ENABLE_AUTO_SAVER);
749             filter.addAction(ACTION_AUTO_SAVER_NO_THANKS);
750             filter.addAction(ACTION_DISMISS_AUTO_SAVER_SUGGESTION);
751             mContext.registerReceiverAsUser(this, UserHandle.ALL, filter,
752                     android.Manifest.permission.DEVICE_POWER, mHandler);
753         }
754 
755         @Override
onReceive(Context context, Intent intent)756         public void onReceive(Context context, Intent intent) {
757             final String action = intent.getAction();
758             Slog.i(TAG, "Received " + action);
759             if (action.equals(ACTION_SHOW_BATTERY_SETTINGS)) {
760                 dismissLowBatteryNotification();
761                 mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT);
762             } else if (action.equals(ACTION_START_SAVER)) {
763                 setSaverMode(true, true);
764                 dismissLowBatteryNotification();
765             } else if (action.equals(ACTION_SHOW_START_SAVER_CONFIRMATION)) {
766                 dismissLowBatteryNotification();
767                 showStartSaverConfirmation(intent.getExtras());
768             } else if (action.equals(ACTION_DISMISSED_WARNING)) {
769                 dismissLowBatteryWarning();
770             } else if (ACTION_CLICKED_TEMP_WARNING.equals(action)) {
771                 dismissHighTemperatureWarningInternal();
772                 showHighTemperatureDialog();
773             } else if (ACTION_DISMISSED_TEMP_WARNING.equals(action)) {
774                 dismissHighTemperatureWarningInternal();
775             } else if (ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING.equals(action)) {
776                 dismissThermalShutdownWarning();
777                 showThermalShutdownDialog();
778             } else if (ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING.equals(action)) {
779                 dismissThermalShutdownWarning();
780             } else if (ACTION_SHOW_AUTO_SAVER_SUGGESTION.equals(action)) {
781                 showAutoSaverSuggestion();
782             } else if (ACTION_DISMISS_AUTO_SAVER_SUGGESTION.equals(action)) {
783                 dismissAutoSaverSuggestion();
784             } else if (ACTION_ENABLE_AUTO_SAVER.equals(action)) {
785                 dismissAutoSaverSuggestion();
786                 startBatterySaverSchedulePage();
787             } else if (ACTION_AUTO_SAVER_NO_THANKS.equals(action)) {
788                 dismissAutoSaverSuggestion();
789                 BatterySaverUtils.suppressAutoBatterySaver(context);
790             }
791         }
792     }
793 }
794