blob: a20f68753a78fb21cc710d821a45261d7e2e8e71 [file] [log] [blame]
Beverly312ad022018-01-24 15:07:48 -05001package com.android.settingslib.notification;
2
3/*
4 * Copyright (C) 2018 The Android Open Source Project
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19import android.app.ActivityManager;
20import android.app.AlarmManager;
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.app.NotificationManager;
24import android.content.Context;
25import android.content.DialogInterface;
26import android.net.Uri;
27import android.provider.Settings;
28import android.service.notification.Condition;
29import android.service.notification.ZenModeConfig;
30import android.text.TextUtils;
31import android.util.Log;
32import android.util.Slog;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.widget.CompoundButton;
36import android.widget.ImageView;
37import android.widget.LinearLayout;
38import android.widget.RadioButton;
39import android.widget.RadioGroup;
40import android.widget.ScrollView;
41import android.widget.TextView;
42
43import com.android.internal.logging.MetricsLogger;
44import com.android.internal.logging.nano.MetricsProto;
45import com.android.internal.policy.PhoneWindow;
46import com.android.settingslib.R;
47
48import java.util.Arrays;
49import java.util.Calendar;
50import java.util.GregorianCalendar;
51import java.util.Objects;
52
53public class EnableZenModeDialog {
54
55 private static final String TAG = "QSEnableZenModeDialog";
56 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
57
58 private static final int[] MINUTE_BUCKETS = ZenModeConfig.MINUTE_BUCKETS;
59 private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
60 private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
61 private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
62
63 private static final int FOREVER_CONDITION_INDEX = 0;
64 private static final int COUNTDOWN_CONDITION_INDEX = 1;
65 private static final int COUNTDOWN_ALARM_CONDITION_INDEX = 2;
66
67 private static final int SECONDS_MS = 1000;
68 private static final int MINUTES_MS = 60 * SECONDS_MS;
69
70 private Uri mForeverId;
71 private int mBucketIndex = -1;
72
73 private AlarmManager mAlarmManager;
74 private int mUserId;
75 private boolean mAttached;
76
77 private Context mContext;
78 private RadioGroup mZenRadioGroup;
79 private LinearLayout mZenRadioGroupContent;
80 private int MAX_MANUAL_DND_OPTIONS = 3;
81
82 public EnableZenModeDialog(Context context) {
83 mContext = context;
84 }
85
86 public Dialog createDialog() {
87 NotificationManager noMan = (NotificationManager) mContext.
88 getSystemService(Context.NOTIFICATION_SERVICE);
89 mForeverId = Condition.newId(mContext).appendPath("forever").build();
90 mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
91 mUserId = mContext.getUserId();
92 mAttached = false;
93
94 final AlertDialog.Builder builder = new AlertDialog.Builder(mContext)
95 .setTitle(R.string.zen_mode_settings_turn_on_dialog_title)
96 .setNegativeButton(R.string.cancel, null)
97 .setPositiveButton(R.string.zen_mode_enable_dialog_turn_on,
98 new DialogInterface.OnClickListener() {
99 @Override
100 public void onClick(DialogInterface dialog, int which) {
101 int checkedId = mZenRadioGroup.getCheckedRadioButtonId();
102 ConditionTag tag = getConditionTagAt(checkedId);
103
104 if (isForever(tag.condition)) {
105 MetricsLogger.action(mContext,
106 MetricsProto.MetricsEvent.
107 NOTIFICATION_ZEN_MODE_TOGGLE_ON_FOREVER);
108 } else if (isAlarm(tag.condition)) {
109 MetricsLogger.action(mContext,
110 MetricsProto.MetricsEvent.
111 NOTIFICATION_ZEN_MODE_TOGGLE_ON_ALARM);
112 } else if (isCountdown(tag.condition)) {
113 MetricsLogger.action(mContext,
114 MetricsProto.MetricsEvent.
115 NOTIFICATION_ZEN_MODE_TOGGLE_ON_COUNTDOWN);
116 } else {
117 Slog.d(TAG, "Invalid manual condition: " + tag.condition);
118 }
119 // always triggers priority-only dnd with chosen condition
120 noMan.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
121 getRealConditionId(tag.condition), TAG);
122 }
123 });
124
125 View contentView = getContentView();
126 bindConditions(forever());
127 builder.setView(contentView);
128 return builder.create();
129 }
130
131 private void hideAllConditions() {
132 final int N = mZenRadioGroupContent.getChildCount();
133 for (int i = 0; i < N; i++) {
134 mZenRadioGroupContent.getChildAt(i).setVisibility(View.GONE);
135 }
136 }
137
138 protected View getContentView() {
139 final LayoutInflater inflater = new PhoneWindow(mContext).getLayoutInflater();
140 View contentView = inflater.inflate(R.layout.zen_mode_turn_on_dialog_container, null);
141 ScrollView container = (ScrollView) contentView.findViewById(R.id.container);
142
143 mZenRadioGroup = container.findViewById(R.id.zen_radio_buttons);
144 mZenRadioGroupContent = container.findViewById(R.id.zen_radio_buttons_content);
145
146 for (int i = 0; i < MAX_MANUAL_DND_OPTIONS; i++) {
147 final View radioButton = inflater.inflate(R.layout.zen_mode_radio_button,
148 mZenRadioGroup, false);
149 mZenRadioGroup.addView(radioButton);
150 radioButton.setId(i);
151
152 final View radioButtonContent = inflater.inflate(R.layout.zen_mode_condition,
153 mZenRadioGroupContent, false);
154 radioButtonContent.setId(i + MAX_MANUAL_DND_OPTIONS);
155 mZenRadioGroupContent.addView(radioButtonContent);
156 }
157 hideAllConditions();
158 return contentView;
159 }
160
161 private void bind(final Condition condition, final View row, final int rowId) {
162 if (condition == null) throw new IllegalArgumentException("condition must not be null");
163 final boolean enabled = condition.state == Condition.STATE_TRUE;
164 final ConditionTag tag = row.getTag() != null ? (ConditionTag) row.getTag() :
165 new ConditionTag();
166 row.setTag(tag);
167 final boolean first = tag.rb == null;
168 if (tag.rb == null) {
169 tag.rb = (RadioButton) mZenRadioGroup.getChildAt(rowId);
170 }
171 tag.condition = condition;
172 final Uri conditionId = getConditionId(tag.condition);
173 if (DEBUG) Log.d(TAG, "bind i=" + mZenRadioGroupContent.indexOfChild(row) + " first="
174 + first + " condition=" + conditionId);
175 tag.rb.setEnabled(enabled);
176 tag.rb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
177 @Override
178 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
179 if (isChecked) {
180 tag.rb.setChecked(true);
181 if (DEBUG) Log.d(TAG, "onCheckedChanged " + conditionId);
182 MetricsLogger.action(mContext,
183 MetricsProto.MetricsEvent.QS_DND_CONDITION_SELECT);
184 announceConditionSelection(tag);
185 }
186 }
187 });
188
189 updateUi(tag, row, condition, enabled, rowId, conditionId);
190 row.setVisibility(View.VISIBLE);
191 }
192
193 private ConditionTag getConditionTagAt(int index) {
194 return (ConditionTag) mZenRadioGroupContent.getChildAt(index).getTag();
195 }
196
197 private void bindConditions(Condition c) {
198 // forever
199 bind(forever(), mZenRadioGroupContent.getChildAt(FOREVER_CONDITION_INDEX),
200 FOREVER_CONDITION_INDEX);
201 if (c == null) {
202 bindGenericCountdown();
203 bindNextAlarm(getTimeUntilNextAlarmCondition());
204 } else if (isForever(c)) {
205 getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true);
206 bindGenericCountdown();
207 bindNextAlarm(getTimeUntilNextAlarmCondition());
208 } else {
209 if (isAlarm(c)) {
210 bindGenericCountdown();
211 bindNextAlarm(c);
212 getConditionTagAt(COUNTDOWN_ALARM_CONDITION_INDEX).rb.setChecked(true);
213 } else if (isCountdown(c)) {
214 bindNextAlarm(getTimeUntilNextAlarmCondition());
215 bind(c, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
216 COUNTDOWN_CONDITION_INDEX);
217 getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true);
218 } else {
219 Slog.d(TAG, "Invalid manual condition: " + c);
220 }
221 }
222 }
223
224 public static Uri getConditionId(Condition condition) {
225 return condition != null ? condition.id : null;
226 }
227
228 public Condition forever() {
229 Uri foreverId = Condition.newId(mContext).appendPath("forever").build();
230 return new Condition(foreverId, foreverSummary(mContext), "", "", 0 /*icon*/,
231 Condition.STATE_TRUE, 0 /*flags*/);
232 }
233
234 public long getNextAlarm() {
235 final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(mUserId);
236 return info != null ? info.getTriggerTime() : 0;
237 }
238
239 private boolean isAlarm(Condition c) {
240 return c != null && ZenModeConfig.isValidCountdownToAlarmConditionId(c.id);
241 }
242
243 private boolean isCountdown(Condition c) {
244 return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
245 }
246
247 private boolean isForever(Condition c) {
248 return c != null && mForeverId.equals(c.id);
249 }
250
251 private Uri getRealConditionId(Condition condition) {
252 return isForever(condition) ? null : getConditionId(condition);
253 }
254
255 private String foreverSummary(Context context) {
256 return context.getString(com.android.internal.R.string.zen_mode_forever);
257 }
258
259 private static void setToMidnight(Calendar calendar) {
260 calendar.set(Calendar.HOUR_OF_DAY, 0);
261 calendar.set(Calendar.MINUTE, 0);
262 calendar.set(Calendar.SECOND, 0);
263 calendar.set(Calendar.MILLISECOND, 0);
264 }
265
266 // Returns a time condition if the next alarm is within the next week.
267 private Condition getTimeUntilNextAlarmCondition() {
268 GregorianCalendar weekRange = new GregorianCalendar();
269 setToMidnight(weekRange);
270 weekRange.add(Calendar.DATE, 6);
271 final long nextAlarmMs = getNextAlarm();
272 if (nextAlarmMs > 0) {
273 GregorianCalendar nextAlarm = new GregorianCalendar();
274 nextAlarm.setTimeInMillis(nextAlarmMs);
275 setToMidnight(nextAlarm);
276
277 if (weekRange.compareTo(nextAlarm) >= 0) {
278 return ZenModeConfig.toNextAlarmCondition(mContext, nextAlarmMs,
279 ActivityManager.getCurrentUser());
280 }
281 }
282 return null;
283 }
284
285 private void bindGenericCountdown() {
286 mBucketIndex = DEFAULT_BUCKET_INDEX;
287 Condition countdown = ZenModeConfig.toTimeCondition(mContext,
288 MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
289 if (!mAttached || getConditionTagAt(COUNTDOWN_CONDITION_INDEX).condition == null) {
290 bind(countdown, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
291 COUNTDOWN_CONDITION_INDEX);
292 }
293 }
294
295 private void updateUi(ConditionTag tag, View row, Condition condition,
296 boolean enabled, int rowId, Uri conditionId) {
297 if (tag.lines == null) {
298 tag.lines = row.findViewById(android.R.id.content);
299 }
300 if (tag.line1 == null) {
301 tag.line1 = (TextView) row.findViewById(android.R.id.text1);
302 }
303
304 if (tag.line2 == null) {
305 tag.line2 = (TextView) row.findViewById(android.R.id.text2);
306 }
307
308 final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1
309 : condition.summary;
310 final String line2 = condition.line2;
311 tag.line1.setText(line1);
312 if (TextUtils.isEmpty(line2)) {
313 tag.line2.setVisibility(View.GONE);
314 } else {
315 tag.line2.setVisibility(View.VISIBLE);
316 tag.line2.setText(line2);
317 }
318 tag.lines.setEnabled(enabled);
319 tag.lines.setAlpha(enabled ? 1 : .4f);
320
321 tag.lines.setOnClickListener(new View.OnClickListener() {
322 @Override
323 public void onClick(View v) {
324 tag.rb.setChecked(true);
325 }
326 });
327
328 // minus button
329 final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
330 button1.setOnClickListener(new View.OnClickListener() {
331 @Override
332 public void onClick(View v) {
333 onClickTimeButton(row, tag, false /*down*/, rowId);
334 }
335 });
336
337 // plus button
338 final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
339 button2.setOnClickListener(new View.OnClickListener() {
340 @Override
341 public void onClick(View v) {
342 onClickTimeButton(row, tag, true /*up*/, rowId);
343 }
344 });
345
346 final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
347 if (rowId == COUNTDOWN_CONDITION_INDEX && time > 0) {
348 button1.setVisibility(View.VISIBLE);
349 button2.setVisibility(View.VISIBLE);
350 if (mBucketIndex > -1) {
351 button1.setEnabled(mBucketIndex > 0);
352 button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
353 } else {
354 final long span = time - System.currentTimeMillis();
355 button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
356 final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
357 MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
358 button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
359 }
360
361 button1.setAlpha(button1.isEnabled() ? 1f : .5f);
362 button2.setAlpha(button2.isEnabled() ? 1f : .5f);
363 } else {
364 button1.setVisibility(View.GONE);
365 button2.setVisibility(View.GONE);
366 }
367 }
368
369 private void bindNextAlarm(Condition c) {
370 View alarmContent = mZenRadioGroupContent.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX);
371 ConditionTag tag = (ConditionTag) alarmContent.getTag();
372
373 if (c != null && (!mAttached || tag == null || tag.condition == null)) {
374 bind(c, alarmContent, COUNTDOWN_ALARM_CONDITION_INDEX);
375 }
376
377 // hide the alarm radio button if there isn't a "next alarm condition"
378 tag = (ConditionTag) alarmContent.getTag();
379 boolean showAlarm = tag != null && tag.condition != null;
380 mZenRadioGroup.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX).setVisibility(
381 showAlarm ? View.VISIBLE : View.GONE);
382 alarmContent.setVisibility(showAlarm ? View.VISIBLE : View.GONE);
383 }
384
385 private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) {
386 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.QS_DND_TIME, up);
387 Condition newCondition = null;
388 final int N = MINUTE_BUCKETS.length;
389 if (mBucketIndex == -1) {
390 // not on a known index, search for the next or prev bucket by time
391 final Uri conditionId = getConditionId(tag.condition);
392 final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
393 final long now = System.currentTimeMillis();
394 for (int i = 0; i < N; i++) {
395 int j = up ? i : N - 1 - i;
396 final int bucketMinutes = MINUTE_BUCKETS[j];
397 final long bucketTime = now + bucketMinutes * MINUTES_MS;
398 if (up && bucketTime > time || !up && bucketTime < time) {
399 mBucketIndex = j;
400 newCondition = ZenModeConfig.toTimeCondition(mContext,
401 bucketTime, bucketMinutes, ActivityManager.getCurrentUser(),
402 false /*shortVersion*/);
403 break;
404 }
405 }
406 if (newCondition == null) {
407 mBucketIndex = DEFAULT_BUCKET_INDEX;
408 newCondition = ZenModeConfig.toTimeCondition(mContext,
409 MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
410 }
411 } else {
412 // on a known index, simply increment or decrement
413 mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
414 newCondition = ZenModeConfig.toTimeCondition(mContext,
415 MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
416 }
417 bind(newCondition, row, rowId);
418 tag.rb.setChecked(true);
419 announceConditionSelection(tag);
420 }
421
422 private void announceConditionSelection(ConditionTag tag) {
423 // condition will always be priority-only
424 String modeText = mContext.getString(R.string.zen_interruption_level_priority);
425 if (tag.line1 != null) {
426 mZenRadioGroupContent.announceForAccessibility(mContext.getString(
427 R.string.zen_mode_and_condition, modeText, tag.line1.getText()));
428 }
429 }
430
431 // used as the view tag on condition rows
432 private static class ConditionTag {
433 public RadioButton rb;
434 public View lines;
435 public TextView line1;
436 public TextView line2;
437 public Condition condition;
438 }
439}