Beverly | 312ad02 | 2018-01-24 15:07:48 -0500 | [diff] [blame] | 1 | package 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 | |
| 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.util.Log; |
| 32 | import android.util.Slog; |
| 33 | import android.view.LayoutInflater; |
| 34 | import android.view.View; |
| 35 | import android.widget.CompoundButton; |
| 36 | import android.widget.ImageView; |
| 37 | import android.widget.LinearLayout; |
| 38 | import android.widget.RadioButton; |
| 39 | import android.widget.RadioGroup; |
| 40 | import android.widget.ScrollView; |
| 41 | import android.widget.TextView; |
| 42 | |
| 43 | import com.android.internal.logging.MetricsLogger; |
| 44 | import com.android.internal.logging.nano.MetricsProto; |
| 45 | import com.android.internal.policy.PhoneWindow; |
| 46 | import com.android.settingslib.R; |
| 47 | |
| 48 | import java.util.Arrays; |
| 49 | import java.util.Calendar; |
| 50 | import java.util.GregorianCalendar; |
| 51 | import java.util.Objects; |
| 52 | |
| 53 | public 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 | } |