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