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