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