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