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