• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.settingslib.notification;
18 
19 import android.app.ActivityManager;
20 import android.app.AlarmManager;
21 import android.app.AlertDialog;
22 import android.app.Dialog;
23 import android.app.NotificationManager;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.net.Uri;
27 import android.provider.Settings;
28 import android.service.notification.Condition;
29 import android.service.notification.ZenModeConfig;
30 import android.text.TextUtils;
31 import android.text.format.DateFormat;
32 import android.util.Log;
33 import android.util.Slog;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.CompoundButton;
38 import android.widget.ImageView;
39 import android.widget.LinearLayout;
40 import android.widget.RadioButton;
41 import android.widget.RadioGroup;
42 import android.widget.ScrollView;
43 import android.widget.TextView;
44 
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.internal.logging.MetricsLogger;
47 import com.android.internal.logging.nano.MetricsProto;
48 import com.android.internal.policy.PhoneWindow;
49 import com.android.settingslib.R;
50 
51 import java.util.Arrays;
52 import java.util.Calendar;
53 import java.util.GregorianCalendar;
54 import java.util.Locale;
55 import java.util.Objects;
56 
57 public class EnableZenModeDialog {
58     private static final String TAG = "EnableZenModeDialog";
59     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
60 
61     private static final int[] MINUTE_BUCKETS = ZenModeConfig.MINUTE_BUCKETS;
62     private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
63     private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
64     private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
65 
66     @VisibleForTesting
67     protected static final int FOREVER_CONDITION_INDEX = 0;
68     @VisibleForTesting
69     protected static final int COUNTDOWN_CONDITION_INDEX = 1;
70     @VisibleForTesting
71     protected static final int COUNTDOWN_ALARM_CONDITION_INDEX = 2;
72 
73     private static final int SECONDS_MS = 1000;
74     private static final int MINUTES_MS = 60 * SECONDS_MS;
75 
76     @VisibleForTesting
77     protected Uri mForeverId;
78     private int mBucketIndex = -1;
79 
80     @VisibleForTesting
81     protected NotificationManager mNotificationManager;
82     private AlarmManager mAlarmManager;
83     private int mUserId;
84     private boolean mAttached;
85 
86     @VisibleForTesting
87     protected Context mContext;
88     @VisibleForTesting
89     protected TextView mZenAlarmWarning;
90     @VisibleForTesting
91     protected LinearLayout mZenRadioGroupContent;
92 
93     private RadioGroup mZenRadioGroup;
94     private int MAX_MANUAL_DND_OPTIONS = 3;
95 
96     @VisibleForTesting
97     protected LayoutInflater mLayoutInflater;
98 
EnableZenModeDialog(Context context)99     public EnableZenModeDialog(Context context) {
100         mContext = context;
101     }
102 
createDialog()103     public Dialog createDialog() {
104         mNotificationManager = (NotificationManager) mContext.
105                 getSystemService(Context.NOTIFICATION_SERVICE);
106         mForeverId =  Condition.newId(mContext).appendPath("forever").build();
107         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
108         mUserId = mContext.getUserId();
109         mAttached = false;
110 
111         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext)
112                 .setTitle(R.string.zen_mode_settings_turn_on_dialog_title)
113                 .setNegativeButton(R.string.cancel, null)
114                 .setPositiveButton(R.string.zen_mode_enable_dialog_turn_on,
115                         new DialogInterface.OnClickListener() {
116                             @Override
117                             public void onClick(DialogInterface dialog, int which) {
118                                 int checkedId = mZenRadioGroup.getCheckedRadioButtonId();
119                                 ConditionTag tag = getConditionTagAt(checkedId);
120 
121                                 if (isForever(tag.condition)) {
122                                     MetricsLogger.action(mContext,
123                                             MetricsProto.MetricsEvent.
124                                                     NOTIFICATION_ZEN_MODE_TOGGLE_ON_FOREVER);
125                                 } else if (isAlarm(tag.condition)) {
126                                     MetricsLogger.action(mContext,
127                                             MetricsProto.MetricsEvent.
128                                                     NOTIFICATION_ZEN_MODE_TOGGLE_ON_ALARM);
129                                 } else if (isCountdown(tag.condition)) {
130                                     MetricsLogger.action(mContext,
131                                             MetricsProto.MetricsEvent.
132                                                     NOTIFICATION_ZEN_MODE_TOGGLE_ON_COUNTDOWN);
133                                 } else {
134                                     Slog.d(TAG, "Invalid manual condition: " + tag.condition);
135                                 }
136                                 // always triggers priority-only dnd with chosen condition
137                                 mNotificationManager.setZenMode(
138                                         Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
139                                         getRealConditionId(tag.condition), TAG);
140                             }
141                         });
142 
143         View contentView = getContentView();
144         bindConditions(forever());
145         builder.setView(contentView);
146         return builder.create();
147     }
148 
hideAllConditions()149     private void hideAllConditions() {
150         final int N = mZenRadioGroupContent.getChildCount();
151         for (int i = 0; i < N; i++) {
152             mZenRadioGroupContent.getChildAt(i).setVisibility(View.GONE);
153         }
154 
155         mZenAlarmWarning.setVisibility(View.GONE);
156     }
157 
getContentView()158     protected View getContentView() {
159         if (mLayoutInflater == null) {
160             mLayoutInflater = new PhoneWindow(mContext).getLayoutInflater();
161         }
162         View contentView = mLayoutInflater.inflate(R.layout.zen_mode_turn_on_dialog_container,
163                 null);
164         ScrollView container = (ScrollView) contentView.findViewById(R.id.container);
165 
166         mZenRadioGroup = container.findViewById(R.id.zen_radio_buttons);
167         mZenRadioGroupContent = container.findViewById(R.id.zen_radio_buttons_content);
168         mZenAlarmWarning = container.findViewById(R.id.zen_alarm_warning);
169 
170         for (int i = 0; i < MAX_MANUAL_DND_OPTIONS; i++) {
171             final View radioButton = mLayoutInflater.inflate(R.layout.zen_mode_radio_button,
172                     mZenRadioGroup, false);
173             mZenRadioGroup.addView(radioButton);
174             radioButton.setId(i);
175 
176             final View radioButtonContent = mLayoutInflater.inflate(R.layout.zen_mode_condition,
177                     mZenRadioGroupContent, false);
178             radioButtonContent.setId(i + MAX_MANUAL_DND_OPTIONS);
179             mZenRadioGroupContent.addView(radioButtonContent);
180         }
181 
182         hideAllConditions();
183         return contentView;
184     }
185 
186     @VisibleForTesting
bind(final Condition condition, final View row, final int rowId)187     protected void bind(final Condition condition, final View row, final int rowId) {
188         if (condition == null) throw new IllegalArgumentException("condition must not be null");
189 
190         final boolean enabled = condition.state == Condition.STATE_TRUE;
191         final ConditionTag tag = row.getTag() != null ? (ConditionTag) row.getTag() :
192                 new ConditionTag();
193         row.setTag(tag);
194         final boolean first = tag.rb == null;
195         if (tag.rb == null) {
196             tag.rb = (RadioButton) mZenRadioGroup.getChildAt(rowId);
197         }
198         tag.condition = condition;
199         final Uri conditionId = getConditionId(tag.condition);
200         if (DEBUG) Log.d(TAG, "bind i=" + mZenRadioGroupContent.indexOfChild(row) + " first="
201                 + first + " condition=" + conditionId);
202         tag.rb.setEnabled(enabled);
203         tag.rb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
204             @Override
205             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
206                 if (isChecked) {
207                     tag.rb.setChecked(true);
208                     if (DEBUG) Log.d(TAG, "onCheckedChanged " + conditionId);
209                     MetricsLogger.action(mContext,
210                             MetricsProto.MetricsEvent.QS_DND_CONDITION_SELECT);
211                     updateAlarmWarningText(tag.condition);
212                 }
213             }
214         });
215 
216         updateUi(tag, row, condition, enabled, rowId, conditionId);
217         row.setVisibility(View.VISIBLE);
218     }
219 
220     @VisibleForTesting
getConditionTagAt(int index)221     protected ConditionTag getConditionTagAt(int index) {
222         return (ConditionTag) mZenRadioGroupContent.getChildAt(index).getTag();
223     }
224 
225     @VisibleForTesting
bindConditions(Condition c)226     protected void bindConditions(Condition c) {
227         // forever
228         bind(forever(), mZenRadioGroupContent.getChildAt(FOREVER_CONDITION_INDEX),
229                 FOREVER_CONDITION_INDEX);
230         if (c == null) {
231             bindGenericCountdown();
232             bindNextAlarm(getTimeUntilNextAlarmCondition());
233         } else if (isForever(c)) {
234             getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true);
235             bindGenericCountdown();
236             bindNextAlarm(getTimeUntilNextAlarmCondition());
237         } else {
238             if (isAlarm(c)) {
239                 bindGenericCountdown();
240                 bindNextAlarm(c);
241                 getConditionTagAt(COUNTDOWN_ALARM_CONDITION_INDEX).rb.setChecked(true);
242             } else if (isCountdown(c)) {
243                 bindNextAlarm(getTimeUntilNextAlarmCondition());
244                 bind(c, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
245                         COUNTDOWN_CONDITION_INDEX);
246                 getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true);
247             } else {
248                 Slog.d(TAG, "Invalid manual condition: " + c);
249             }
250         }
251     }
252 
getConditionId(Condition condition)253     public static Uri getConditionId(Condition condition) {
254         return condition != null ? condition.id : null;
255     }
256 
forever()257     public Condition forever() {
258         Uri foreverId = Condition.newId(mContext).appendPath("forever").build();
259         return new Condition(foreverId, foreverSummary(mContext), "", "", 0 /*icon*/,
260                 Condition.STATE_TRUE, 0 /*flags*/);
261     }
262 
getNextAlarm()263     public long getNextAlarm() {
264         final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(mUserId);
265         return info != null ? info.getTriggerTime() : 0;
266     }
267 
268     @VisibleForTesting
isAlarm(Condition c)269     protected boolean isAlarm(Condition c) {
270         return c != null && ZenModeConfig.isValidCountdownToAlarmConditionId(c.id);
271     }
272 
273     @VisibleForTesting
isCountdown(Condition c)274     protected boolean isCountdown(Condition c) {
275         return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
276     }
277 
isForever(Condition c)278     private boolean isForever(Condition c) {
279         return c != null && mForeverId.equals(c.id);
280     }
281 
getRealConditionId(Condition condition)282     private Uri getRealConditionId(Condition condition) {
283         return isForever(condition) ? null : getConditionId(condition);
284     }
285 
foreverSummary(Context context)286     private String foreverSummary(Context context) {
287         return context.getString(com.android.internal.R.string.zen_mode_forever);
288     }
289 
setToMidnight(Calendar calendar)290     private static void setToMidnight(Calendar calendar) {
291         calendar.set(Calendar.HOUR_OF_DAY, 0);
292         calendar.set(Calendar.MINUTE, 0);
293         calendar.set(Calendar.SECOND, 0);
294         calendar.set(Calendar.MILLISECOND, 0);
295     }
296 
297     // Returns a time condition if the next alarm is within the next week.
298     @VisibleForTesting
getTimeUntilNextAlarmCondition()299     protected Condition getTimeUntilNextAlarmCondition() {
300         GregorianCalendar weekRange = new GregorianCalendar();
301         setToMidnight(weekRange);
302         weekRange.add(Calendar.DATE, 6);
303         final long nextAlarmMs = getNextAlarm();
304         if (nextAlarmMs > 0) {
305             GregorianCalendar nextAlarm = new GregorianCalendar();
306             nextAlarm.setTimeInMillis(nextAlarmMs);
307             setToMidnight(nextAlarm);
308 
309             if (weekRange.compareTo(nextAlarm) >= 0) {
310                 return ZenModeConfig.toNextAlarmCondition(mContext, nextAlarmMs,
311                         ActivityManager.getCurrentUser());
312             }
313         }
314         return null;
315     }
316 
317     @VisibleForTesting
bindGenericCountdown()318     protected void bindGenericCountdown() {
319         mBucketIndex = DEFAULT_BUCKET_INDEX;
320         Condition countdown = ZenModeConfig.toTimeCondition(mContext,
321                 MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
322         if (!mAttached || getConditionTagAt(COUNTDOWN_CONDITION_INDEX).condition == null) {
323             bind(countdown, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
324                     COUNTDOWN_CONDITION_INDEX);
325         }
326     }
327 
updateUi(ConditionTag tag, View row, Condition condition, boolean enabled, int rowId, Uri conditionId)328     private void updateUi(ConditionTag tag, View row, Condition condition,
329             boolean enabled, int rowId, Uri conditionId) {
330         if (tag.lines == null) {
331             tag.lines = row.findViewById(android.R.id.content);
332         }
333         if (tag.line1 == null) {
334             tag.line1 = (TextView) row.findViewById(android.R.id.text1);
335         }
336 
337         if (tag.line2 == null) {
338             tag.line2 = (TextView) row.findViewById(android.R.id.text2);
339         }
340 
341         final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1
342                 : condition.summary;
343         final String line2 = condition.line2;
344         tag.line1.setText(line1);
345         if (TextUtils.isEmpty(line2)) {
346             tag.line2.setVisibility(View.GONE);
347         } else {
348             tag.line2.setVisibility(View.VISIBLE);
349             tag.line2.setText(line2);
350         }
351         tag.lines.setEnabled(enabled);
352         tag.lines.setAlpha(enabled ? 1 : .4f);
353 
354         tag.lines.setOnClickListener(new View.OnClickListener() {
355             @Override
356             public void onClick(View v) {
357                 tag.rb.setChecked(true);
358             }
359         });
360 
361         final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
362         final ImageView minusButton = (ImageView) row.findViewById(android.R.id.button1);
363         final ImageView plusButton = (ImageView) row.findViewById(android.R.id.button2);
364         if (rowId == COUNTDOWN_CONDITION_INDEX && time > 0) {
365             minusButton.setOnClickListener(new View.OnClickListener() {
366                 @Override
367                 public void onClick(View v) {
368                     onClickTimeButton(row, tag, false /*down*/, rowId);
369                     tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
370                 }
371             });
372 
373             plusButton.setOnClickListener(new View.OnClickListener() {
374                 @Override
375                 public void onClick(View v) {
376                     onClickTimeButton(row, tag, true /*up*/, rowId);
377                     tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
378                 }
379             });
380             if (mBucketIndex > -1) {
381                 minusButton.setEnabled(mBucketIndex > 0);
382                 plusButton.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
383             } else {
384                 final long span = time - System.currentTimeMillis();
385                 minusButton.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
386                 final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
387                         MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
388                 plusButton.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
389             }
390 
391             minusButton.setAlpha(minusButton.isEnabled() ? 1f : .5f);
392             plusButton.setAlpha(plusButton.isEnabled() ? 1f : .5f);
393         } else {
394             if (minusButton != null) {
395                 ((ViewGroup) row).removeView(minusButton);
396             }
397 
398             if (plusButton != null) {
399                 ((ViewGroup) row).removeView(plusButton);
400             }
401         }
402     }
403 
404     @VisibleForTesting
bindNextAlarm(Condition c)405     protected void bindNextAlarm(Condition c) {
406         View alarmContent = mZenRadioGroupContent.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX);
407         ConditionTag tag = (ConditionTag) alarmContent.getTag();
408 
409         if (c != null && (!mAttached || tag == null || tag.condition == null)) {
410             bind(c, alarmContent, COUNTDOWN_ALARM_CONDITION_INDEX);
411         }
412 
413         // hide the alarm radio button if there isn't a "next alarm condition"
414         tag = (ConditionTag) alarmContent.getTag();
415         boolean showAlarm = tag != null && tag.condition != null;
416         mZenRadioGroup.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX).setVisibility(
417                 showAlarm ? View.VISIBLE : View.GONE);
418         alarmContent.setVisibility(showAlarm ? View.VISIBLE : View.GONE);
419     }
420 
onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId)421     private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) {
422         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.QS_DND_TIME, up);
423         Condition newCondition = null;
424         final int N = MINUTE_BUCKETS.length;
425         if (mBucketIndex == -1) {
426             // not on a known index, search for the next or prev bucket by time
427             final Uri conditionId = getConditionId(tag.condition);
428             final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
429             final long now = System.currentTimeMillis();
430             for (int i = 0; i < N; i++) {
431                 int j = up ? i : N - 1 - i;
432                 final int bucketMinutes = MINUTE_BUCKETS[j];
433                 final long bucketTime = now + bucketMinutes * MINUTES_MS;
434                 if (up && bucketTime > time || !up && bucketTime < time) {
435                     mBucketIndex = j;
436                     newCondition = ZenModeConfig.toTimeCondition(mContext,
437                             bucketTime, bucketMinutes, ActivityManager.getCurrentUser(),
438                             false /*shortVersion*/);
439                     break;
440                 }
441             }
442             if (newCondition == null) {
443                 mBucketIndex = DEFAULT_BUCKET_INDEX;
444                 newCondition = ZenModeConfig.toTimeCondition(mContext,
445                         MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
446             }
447         } else {
448             // on a known index, simply increment or decrement
449             mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
450             newCondition = ZenModeConfig.toTimeCondition(mContext,
451                     MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
452         }
453         bind(newCondition, row, rowId);
454         updateAlarmWarningText(tag.condition);
455         tag.rb.setChecked(true);
456     }
457 
updateAlarmWarningText(Condition condition)458     private void updateAlarmWarningText(Condition condition) {
459         String warningText = computeAlarmWarningText(condition);
460         mZenAlarmWarning.setText(warningText);
461         mZenAlarmWarning.setVisibility(warningText == null ? View.GONE : View.VISIBLE);
462     }
463 
464     @VisibleForTesting
computeAlarmWarningText(Condition condition)465     protected String computeAlarmWarningText(Condition condition) {
466         boolean allowAlarms = (mNotificationManager.getNotificationPolicy().priorityCategories
467                 & NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS) != 0;
468 
469         // don't show alarm warning if alarms are allowed to bypass dnd
470         if (allowAlarms) {
471             return null;
472         }
473 
474         final long now = System.currentTimeMillis();
475         final long nextAlarm = getNextAlarm();
476         if (nextAlarm < now) {
477             return null;
478         }
479         int warningRes = 0;
480         if (condition == null || isForever(condition)) {
481             warningRes = R.string.zen_alarm_warning_indef;
482         } else {
483             final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id);
484             if (time > now && nextAlarm < time) {
485                 warningRes = R.string.zen_alarm_warning;
486             }
487         }
488         if (warningRes == 0) {
489             return null;
490         }
491 
492         return mContext.getResources().getString(warningRes, getTime(nextAlarm, now));
493     }
494 
495     @VisibleForTesting
getTime(long nextAlarm, long now)496     protected String getTime(long nextAlarm, long now) {
497         final boolean soon = (nextAlarm - now) < 24 * 60 * 60 * 1000;
498         final boolean is24 = DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser());
499         final String skeleton = soon ? (is24 ? "Hm" : "hma") : (is24 ? "EEEHm" : "EEEhma");
500         final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
501         final CharSequence formattedTime = DateFormat.format(pattern, nextAlarm);
502         final int templateRes = soon ? R.string.alarm_template : R.string.alarm_template_far;
503         return mContext.getResources().getString(templateRes, formattedTime);
504     }
505 
506     // used as the view tag on condition rows
507     @VisibleForTesting
508     protected static class ConditionTag {
509         public RadioButton rb;
510         public View lines;
511         public TextView line1;
512         public TextView line2;
513         public Condition condition;
514     }
515 }
516