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.NotificationManager;
     21 import android.app.PendingIntent;
     22 import android.content.BroadcastReceiver;
     23 import android.content.ContentResolver;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.content.DialogInterface.OnClickListener;
     27 import android.content.DialogInterface.OnDismissListener;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.media.AudioAttributes;
     31 import android.net.Uri;
     32 import android.os.AsyncTask;
     33 import android.os.Handler;
     34 import android.os.PowerManager;
     35 import android.os.SystemClock;
     36 import android.os.UserHandle;
     37 import android.provider.Settings;
     38 import android.util.Slog;
     39 import android.view.View;
     40 
     41 import com.android.systemui.R;
     42 import com.android.systemui.statusbar.phone.PhoneStatusBar;
     43 import com.android.systemui.statusbar.phone.SystemUIDialog;
     44 
     45 import java.io.PrintWriter;
     46 import java.text.NumberFormat;
     47 
     48 public class PowerNotificationWarnings implements PowerUI.WarningsUI {
     49     private static final String TAG = PowerUI.TAG + ".Notification";
     50     private static final boolean DEBUG = PowerUI.DEBUG;
     51 
     52     private static final String TAG_NOTIFICATION = "low_battery";
     53 
     54     private static final int SHOWING_NOTHING = 0;
     55     private static final int SHOWING_WARNING = 1;
     56     private static final int SHOWING_SAVER = 2;
     57     private static final int SHOWING_INVALID_CHARGER = 3;
     58     private static final String[] SHOWING_STRINGS = {
     59         "SHOWING_NOTHING",
     60         "SHOWING_WARNING",
     61         "SHOWING_SAVER",
     62         "SHOWING_INVALID_CHARGER",
     63     };
     64 
     65     private static final String ACTION_SHOW_BATTERY_SETTINGS = "PNW.batterySettings";
     66     private static final String ACTION_START_SAVER = "PNW.startSaver";
     67     private static final String ACTION_STOP_SAVER = "PNW.stopSaver";
     68     private static final String ACTION_DISMISSED_WARNING = "PNW.dismissedWarning";
     69 
     70     private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
     71             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
     72             .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
     73             .build();
     74 
     75     private final Context mContext;
     76     private final NotificationManager mNoMan;
     77     private final PowerManager mPowerMan;
     78     private final Handler mHandler = new Handler();
     79     private final Receiver mReceiver = new Receiver();
     80     private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY);
     81     private final Intent mOpenSaverSettings = settings(Settings.ACTION_BATTERY_SAVER_SETTINGS);
     82 
     83     private int mBatteryLevel;
     84     private int mBucket;
     85     private long mScreenOffTime;
     86     private int mShowing;
     87 
     88     private long mBucketDroppedNegativeTimeMs;
     89 
     90     private boolean mSaver;
     91     private boolean mWarning;
     92     private boolean mPlaySound;
     93     private boolean mInvalidCharger;
     94     private SystemUIDialog mSaverConfirmation;
     95 
     96     public PowerNotificationWarnings(Context context, PhoneStatusBar phoneStatusBar) {
     97         mContext = context;
     98         mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
     99         mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    100         mReceiver.init();
    101     }
    102 
    103     @Override
    104     public void dump(PrintWriter pw) {
    105         pw.print("mSaver="); pw.println(mSaver);
    106         pw.print("mWarning="); pw.println(mWarning);
    107         pw.print("mPlaySound="); pw.println(mPlaySound);
    108         pw.print("mInvalidCharger="); pw.println(mInvalidCharger);
    109         pw.print("mShowing="); pw.println(SHOWING_STRINGS[mShowing]);
    110         pw.print("mSaverConfirmation="); pw.println(mSaverConfirmation != null ? "not null" : null);
    111     }
    112 
    113     @Override
    114     public void update(int batteryLevel, int bucket, long screenOffTime) {
    115         mBatteryLevel = batteryLevel;
    116         if (bucket >= 0) {
    117             mBucketDroppedNegativeTimeMs = 0;
    118         } else if (bucket < mBucket) {
    119             mBucketDroppedNegativeTimeMs = System.currentTimeMillis();
    120         }
    121         mBucket = bucket;
    122         mScreenOffTime = screenOffTime;
    123     }
    124 
    125     @Override
    126     public void showSaverMode(boolean mode) {
    127         mSaver = mode;
    128         if (mSaver && mSaverConfirmation != null) {
    129             mSaverConfirmation.dismiss();
    130         }
    131         updateNotification();
    132     }
    133 
    134     private void updateNotification() {
    135         if (DEBUG) Slog.d(TAG, "updateNotification mWarning=" + mWarning + " mPlaySound="
    136                 + mPlaySound + " mSaver=" + mSaver + " mInvalidCharger=" + mInvalidCharger);
    137         if (mInvalidCharger) {
    138             showInvalidChargerNotification();
    139             mShowing = SHOWING_INVALID_CHARGER;
    140         } else if (mWarning) {
    141             showWarningNotification();
    142             mShowing = SHOWING_WARNING;
    143         } else if (mSaver) {
    144             showSaverNotification();
    145             mShowing = SHOWING_SAVER;
    146         } else {
    147             mNoMan.cancelAsUser(TAG_NOTIFICATION, R.id.notification_power, UserHandle.ALL);
    148             mShowing = SHOWING_NOTHING;
    149         }
    150     }
    151 
    152     private void showInvalidChargerNotification() {
    153         final Notification.Builder nb = new Notification.Builder(mContext)
    154                 .setSmallIcon(R.drawable.ic_power_low)
    155                 .setWhen(0)
    156                 .setShowWhen(false)
    157                 .setOngoing(true)
    158                 .setContentTitle(mContext.getString(R.string.invalid_charger_title))
    159                 .setContentText(mContext.getString(R.string.invalid_charger_text))
    160                 .setPriority(Notification.PRIORITY_MAX)
    161                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    162                 .setColor(mContext.getColor(
    163                         com.android.internal.R.color.system_notification_accent_color));
    164         final Notification n = nb.build();
    165         if (n.headsUpContentView != null) {
    166             n.headsUpContentView.setViewVisibility(com.android.internal.R.id.right_icon, View.GONE);
    167         }
    168         mNoMan.notifyAsUser(TAG_NOTIFICATION, R.id.notification_power, n, UserHandle.ALL);
    169     }
    170 
    171     private void showWarningNotification() {
    172         final int textRes = mSaver ? R.string.battery_low_percent_format_saver_started
    173                 : R.string.battery_low_percent_format;
    174         final String percentage = NumberFormat.getPercentInstance().format((double) mBatteryLevel / 100.0);
    175         final Notification.Builder nb = new Notification.Builder(mContext)
    176                 .setSmallIcon(R.drawable.ic_power_low)
    177                 // Bump the notification when the bucket dropped.
    178                 .setWhen(mBucketDroppedNegativeTimeMs)
    179                 .setShowWhen(false)
    180                 .setContentTitle(mContext.getString(R.string.battery_low_title))
    181                 .setContentText(mContext.getString(textRes, percentage))
    182                 .setOnlyAlertOnce(true)
    183                 .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING))
    184                 .setPriority(Notification.PRIORITY_MAX)
    185                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    186                 .setColor(mContext.getColor(
    187                         com.android.internal.R.color.battery_saver_mode_color));
    188         if (hasBatterySettings()) {
    189             nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS));
    190         }
    191         if (!mSaver) {
    192             nb.addAction(0,
    193                     mContext.getString(R.string.battery_saver_start_action),
    194                     pendingBroadcast(ACTION_START_SAVER));
    195         } else {
    196             addStopSaverAction(nb);
    197         }
    198         if (mPlaySound) {
    199             attachLowBatterySound(nb);
    200             mPlaySound = false;
    201         }
    202         final Notification n = nb.build();
    203         if (n.headsUpContentView != null) {
    204             n.headsUpContentView.setViewVisibility(com.android.internal.R.id.right_icon, View.GONE);
    205         }
    206         mNoMan.notifyAsUser(TAG_NOTIFICATION, R.id.notification_power, n, UserHandle.ALL);
    207     }
    208 
    209     private void showSaverNotification() {
    210         final Notification.Builder nb = new Notification.Builder(mContext)
    211                 .setSmallIcon(R.drawable.ic_power_saver)
    212                 .setContentTitle(mContext.getString(R.string.battery_saver_notification_title))
    213                 .setContentText(mContext.getString(R.string.battery_saver_notification_text))
    214                 .setOngoing(true)
    215                 .setShowWhen(false)
    216                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    217                 .setColor(mContext.getColor(
    218                         com.android.internal.R.color.battery_saver_mode_color));
    219         addStopSaverAction(nb);
    220         if (hasSaverSettings()) {
    221             nb.setContentIntent(pendingActivity(mOpenSaverSettings));
    222         }
    223         mNoMan.notifyAsUser(TAG_NOTIFICATION, R.id.notification_power, nb.build(), UserHandle.ALL);
    224     }
    225 
    226     private void addStopSaverAction(Notification.Builder nb) {
    227         nb.addAction(0,
    228                 mContext.getString(R.string.battery_saver_notification_action_text),
    229                 pendingBroadcast(ACTION_STOP_SAVER));
    230     }
    231 
    232     private void dismissSaverNotification() {
    233         if (mSaver) Slog.i(TAG, "dismissing saver notification");
    234         mSaver = false;
    235         updateNotification();
    236     }
    237 
    238     private PendingIntent pendingActivity(Intent intent) {
    239         return PendingIntent.getActivityAsUser(mContext,
    240                 0, intent, 0, null, UserHandle.CURRENT);
    241     }
    242 
    243     private PendingIntent pendingBroadcast(String action) {
    244         return PendingIntent.getBroadcastAsUser(mContext,
    245                 0, new Intent(action), 0, UserHandle.CURRENT);
    246     }
    247 
    248     private static Intent settings(String action) {
    249         return new Intent(action).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
    250                 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
    251                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    252                 | Intent.FLAG_ACTIVITY_NO_HISTORY
    253                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    254     }
    255 
    256     @Override
    257     public boolean isInvalidChargerWarningShowing() {
    258         return mInvalidCharger;
    259     }
    260 
    261     @Override
    262     public void updateLowBatteryWarning() {
    263         updateNotification();
    264     }
    265 
    266     @Override
    267     public void dismissLowBatteryWarning() {
    268         if (DEBUG) Slog.d(TAG, "dismissing low battery warning: level=" + mBatteryLevel);
    269         dismissLowBatteryNotification();
    270     }
    271 
    272     private void dismissLowBatteryNotification() {
    273         if (mWarning) Slog.i(TAG, "dismissing low battery notification");
    274         mWarning = false;
    275         updateNotification();
    276     }
    277 
    278     private boolean hasBatterySettings() {
    279         return mOpenBatterySettings.resolveActivity(mContext.getPackageManager()) != null;
    280     }
    281 
    282     private boolean hasSaverSettings() {
    283         return mOpenSaverSettings.resolveActivity(mContext.getPackageManager()) != null;
    284     }
    285 
    286     @Override
    287     public void showLowBatteryWarning(boolean playSound) {
    288         Slog.i(TAG,
    289                 "show low battery warning: level=" + mBatteryLevel
    290                 + " [" + mBucket + "] playSound=" + playSound);
    291         mPlaySound = playSound;
    292         mWarning = true;
    293         updateNotification();
    294     }
    295 
    296     private void attachLowBatterySound(Notification.Builder b) {
    297         final ContentResolver cr = mContext.getContentResolver();
    298 
    299         final int silenceAfter = Settings.Global.getInt(cr,
    300                 Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0);
    301         final long offTime = SystemClock.elapsedRealtime() - mScreenOffTime;
    302         if (silenceAfter > 0
    303                 && mScreenOffTime > 0
    304                 && offTime > silenceAfter) {
    305             Slog.i(TAG, "screen off too long (" + offTime + "ms, limit " + silenceAfter
    306                     + "ms): not waking up the user with low battery sound");
    307             return;
    308         }
    309 
    310         if (DEBUG) {
    311             Slog.d(TAG, "playing low battery sound. pick-a-doop!"); // WOMP-WOMP is deprecated
    312         }
    313 
    314         if (Settings.Global.getInt(cr, Settings.Global.POWER_SOUNDS_ENABLED, 1) == 1) {
    315             final String soundPath = Settings.Global.getString(cr,
    316                     Settings.Global.LOW_BATTERY_SOUND);
    317             if (soundPath != null) {
    318                 final Uri soundUri = Uri.parse("file://" + soundPath);
    319                 if (soundUri != null) {
    320                     b.setSound(soundUri, AUDIO_ATTRIBUTES);
    321                     if (DEBUG) Slog.d(TAG, "playing sound " + soundUri);
    322                 }
    323             }
    324         }
    325     }
    326 
    327     @Override
    328     public void dismissInvalidChargerWarning() {
    329         dismissInvalidChargerNotification();
    330     }
    331 
    332     private void dismissInvalidChargerNotification() {
    333         if (mInvalidCharger) Slog.i(TAG, "dismissing invalid charger notification");
    334         mInvalidCharger = false;
    335         updateNotification();
    336     }
    337 
    338     @Override
    339     public void showInvalidChargerWarning() {
    340         mInvalidCharger = true;
    341         updateNotification();
    342     }
    343 
    344     @Override
    345     public void userSwitched() {
    346         updateNotification();
    347     }
    348 
    349     private void showStartSaverConfirmation() {
    350         if (mSaverConfirmation != null) return;
    351         final SystemUIDialog d = new SystemUIDialog(mContext);
    352         d.setTitle(R.string.battery_saver_confirmation_title);
    353         d.setMessage(com.android.internal.R.string.battery_saver_description);
    354         d.setNegativeButton(android.R.string.cancel, null);
    355         d.setPositiveButton(R.string.battery_saver_confirmation_ok, mStartSaverMode);
    356         d.setShowForAllUsers(true);
    357         d.setOnDismissListener(new OnDismissListener() {
    358             @Override
    359             public void onDismiss(DialogInterface dialog) {
    360                 mSaverConfirmation = null;
    361             }
    362         });
    363         d.show();
    364         mSaverConfirmation = d;
    365     }
    366 
    367     private void setSaverMode(boolean mode) {
    368         mPowerMan.setPowerSaveMode(mode);
    369     }
    370 
    371     private final class Receiver extends BroadcastReceiver {
    372 
    373         public void init() {
    374             IntentFilter filter = new IntentFilter();
    375             filter.addAction(ACTION_SHOW_BATTERY_SETTINGS);
    376             filter.addAction(ACTION_START_SAVER);
    377             filter.addAction(ACTION_STOP_SAVER);
    378             filter.addAction(ACTION_DISMISSED_WARNING);
    379             mContext.registerReceiverAsUser(this, UserHandle.ALL, filter,
    380                     android.Manifest.permission.STATUS_BAR_SERVICE, mHandler);
    381         }
    382 
    383         @Override
    384         public void onReceive(Context context, Intent intent) {
    385             final String action = intent.getAction();
    386             Slog.i(TAG, "Received " + action);
    387             if (action.equals(ACTION_SHOW_BATTERY_SETTINGS)) {
    388                 dismissLowBatteryNotification();
    389                 mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT);
    390             } else if (action.equals(ACTION_START_SAVER)) {
    391                 dismissLowBatteryNotification();
    392                 showStartSaverConfirmation();
    393             } else if (action.equals(ACTION_STOP_SAVER)) {
    394                 dismissSaverNotification();
    395                 dismissLowBatteryNotification();
    396                 setSaverMode(false);
    397             } else if (action.equals(ACTION_DISMISSED_WARNING)) {
    398                 dismissLowBatteryWarning();
    399             }
    400         }
    401     }
    402 
    403     private final OnClickListener mStartSaverMode = new OnClickListener() {
    404         @Override
    405         public void onClick(DialogInterface dialog, int which) {
    406             AsyncTask.execute(new Runnable() {
    407                 @Override
    408                 public void run() {
    409                     setSaverMode(true);
    410                 }
    411             });
    412         }
    413     };
    414 }
    415