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