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.Dialog; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.provider.Settings; 24 import android.service.notification.Condition; 25 import android.service.notification.ZenModeConfig; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.CompoundButton; 30 import android.widget.ImageView; 31 import android.widget.LinearLayout; 32 import android.widget.RadioButton; 33 import android.widget.RadioGroup; 34 import android.widget.ScrollView; 35 import android.widget.TextView; 36 37 import androidx.annotation.VisibleForTesting; 38 import androidx.appcompat.app.AlertDialog; 39 40 import com.android.internal.logging.MetricsLogger; 41 import com.android.internal.logging.nano.MetricsProto; 42 import com.android.internal.policy.PhoneWindow; 43 import com.android.settingslib.R; 44 45 import java.util.Arrays; 46 47 public class ZenDurationDialog { 48 private static final int[] MINUTE_BUCKETS = ZenModeConfig.MINUTE_BUCKETS; 49 @VisibleForTesting protected static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0]; 50 @VisibleForTesting protected static final int MAX_BUCKET_MINUTES = 51 MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1]; 52 private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60); 53 @VisibleForTesting protected int mBucketIndex = -1; 54 55 @VisibleForTesting protected static final int FOREVER_CONDITION_INDEX = 0; 56 @VisibleForTesting protected static final int COUNTDOWN_CONDITION_INDEX = 1; 57 @VisibleForTesting protected static final int ALWAYS_ASK_CONDITION_INDEX = 2; 58 59 @VisibleForTesting protected Context mContext; 60 @VisibleForTesting protected LinearLayout mZenRadioGroupContent; 61 private RadioGroup mZenRadioGroup; 62 private int MAX_MANUAL_DND_OPTIONS = 3; 63 64 @VisibleForTesting protected LayoutInflater mLayoutInflater; 65 ZenDurationDialog(Context context)66 public ZenDurationDialog(Context context) { 67 mContext = context; 68 } 69 createDialog()70 public Dialog createDialog() { 71 final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 72 setupDialog(builder); 73 return builder.create(); 74 } 75 setupDialog(AlertDialog.Builder builder)76 public void setupDialog(AlertDialog.Builder builder) { 77 int zenDuration = Settings.Secure.getInt( 78 mContext.getContentResolver(), Settings.Secure.ZEN_DURATION, 79 Settings.Secure.ZEN_DURATION_FOREVER); 80 81 builder.setTitle(R.string.zen_mode_duration_settings_title) 82 .setNegativeButton(R.string.cancel, null) 83 .setPositiveButton(R.string.okay, 84 new DialogInterface.OnClickListener() { 85 @Override 86 public void onClick(DialogInterface dialog, int which) { 87 updateZenDuration(zenDuration); 88 } 89 }); 90 91 View contentView = getContentView(); 92 setupRadioButtons(zenDuration); 93 builder.setView(contentView); 94 } 95 96 @VisibleForTesting updateZenDuration(int currZenDuration)97 protected void updateZenDuration(int currZenDuration) { 98 final int checkedRadioButtonId = mZenRadioGroup.getCheckedRadioButtonId(); 99 100 int newZenDuration = Settings.Secure.getInt( 101 mContext.getContentResolver(), Settings.Secure.ZEN_DURATION, 102 Settings.Secure.ZEN_DURATION_FOREVER); 103 switch (checkedRadioButtonId) { 104 case FOREVER_CONDITION_INDEX: 105 newZenDuration = Settings.Secure.ZEN_DURATION_FOREVER; 106 MetricsLogger.action(mContext, 107 MetricsProto.MetricsEvent. 108 NOTIFICATION_ZEN_MODE_DURATION_FOREVER); 109 break; 110 case COUNTDOWN_CONDITION_INDEX: 111 ConditionTag tag = getConditionTagAt(checkedRadioButtonId); 112 newZenDuration = tag.countdownZenDuration; 113 MetricsLogger.action(mContext, 114 MetricsProto.MetricsEvent. 115 NOTIFICATION_ZEN_MODE_DURATION_TIME, 116 newZenDuration); 117 break; 118 case ALWAYS_ASK_CONDITION_INDEX: 119 newZenDuration = Settings.Secure.ZEN_DURATION_PROMPT; 120 MetricsLogger.action(mContext, 121 MetricsProto.MetricsEvent. 122 NOTIFICATION_ZEN_MODE_DURATION_PROMPT); 123 break; 124 } 125 126 if (currZenDuration != newZenDuration) { 127 Settings.Secure.putInt(mContext.getContentResolver(), 128 Settings.Secure.ZEN_DURATION, newZenDuration); 129 } 130 } 131 132 @VisibleForTesting getContentView()133 protected View getContentView() { 134 if (mLayoutInflater == null) { 135 mLayoutInflater = new PhoneWindow(mContext).getLayoutInflater(); 136 } 137 View contentView = mLayoutInflater.inflate(R.layout.zen_mode_duration_dialog, 138 null); 139 ScrollView container = (ScrollView) contentView.findViewById(R.id.zen_duration_container); 140 141 mZenRadioGroup = container.findViewById(R.id.zen_radio_buttons); 142 mZenRadioGroupContent = container.findViewById(R.id.zen_radio_buttons_content); 143 144 for (int i = 0; i < MAX_MANUAL_DND_OPTIONS; i++) { 145 final View radioButton = mLayoutInflater.inflate(R.layout.zen_mode_radio_button, 146 mZenRadioGroup, false); 147 mZenRadioGroup.addView(radioButton); 148 radioButton.setId(i); 149 150 final View radioButtonContent = mLayoutInflater.inflate(R.layout.zen_mode_condition, 151 mZenRadioGroupContent, false); 152 radioButtonContent.setId(i + MAX_MANUAL_DND_OPTIONS); 153 mZenRadioGroupContent.addView(radioButtonContent); 154 } 155 156 return contentView; 157 } 158 159 @VisibleForTesting setupRadioButtons(int zenDuration)160 protected void setupRadioButtons(int zenDuration) { 161 int checkedIndex = ALWAYS_ASK_CONDITION_INDEX; 162 if (zenDuration == 0) { 163 checkedIndex = FOREVER_CONDITION_INDEX; 164 } else if (zenDuration > 0) { 165 checkedIndex = COUNTDOWN_CONDITION_INDEX; 166 } 167 168 bindTag(zenDuration, mZenRadioGroupContent.getChildAt(FOREVER_CONDITION_INDEX), 169 FOREVER_CONDITION_INDEX); 170 bindTag(zenDuration, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX), 171 COUNTDOWN_CONDITION_INDEX); 172 bindTag(zenDuration, mZenRadioGroupContent.getChildAt(ALWAYS_ASK_CONDITION_INDEX), 173 ALWAYS_ASK_CONDITION_INDEX); 174 getConditionTagAt(checkedIndex).rb.setChecked(true); 175 } 176 bindTag(final int currZenDuration, final View row, final int rowIndex)177 private void bindTag(final int currZenDuration, final View row, final int rowIndex) { 178 final ConditionTag tag = row.getTag() != null ? (ConditionTag) row.getTag() : 179 new ConditionTag(); 180 row.setTag(tag); 181 182 if (tag.rb == null) { 183 tag.rb = (RadioButton) mZenRadioGroup.getChildAt(rowIndex); 184 } 185 186 // if duration is set to forever or always prompt, then countdown time defaults to 1 hour 187 if (currZenDuration <= 0) { 188 tag.countdownZenDuration = MINUTE_BUCKETS[DEFAULT_BUCKET_INDEX]; 189 } else { 190 tag.countdownZenDuration = currZenDuration; 191 } 192 193 tag.rb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 194 @Override 195 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 196 if (isChecked) { 197 tag.rb.setChecked(true); 198 } 199 } 200 }); 201 202 updateUi(tag, row, rowIndex); 203 } 204 205 @VisibleForTesting getConditionTagAt(int index)206 protected ConditionTag getConditionTagAt(int index) { 207 return (ConditionTag) mZenRadioGroupContent.getChildAt(index).getTag(); 208 } 209 210 setupUi(ConditionTag tag, View row)211 private void setupUi(ConditionTag tag, View row) { 212 if (tag.lines == null) { 213 tag.lines = row.findViewById(android.R.id.content); 214 } 215 216 if (tag.line1 == null) { 217 tag.line1 = (TextView) row.findViewById(android.R.id.text1); 218 } 219 220 // text2 is not used in zen duration dialog 221 row.findViewById(android.R.id.text2).setVisibility(View.GONE); 222 223 tag.lines.setOnClickListener(new View.OnClickListener() { 224 @Override 225 public void onClick(View v) { 226 tag.rb.setChecked(true); 227 } 228 }); 229 } 230 updateButtons(ConditionTag tag, View row, int rowIndex)231 private void updateButtons(ConditionTag tag, View row, int rowIndex) { 232 final ImageView minusButton = (ImageView) row.findViewById(android.R.id.button1); 233 final ImageView plusButton = (ImageView) row.findViewById(android.R.id.button2); 234 final long time = tag.countdownZenDuration; 235 if (rowIndex == COUNTDOWN_CONDITION_INDEX) { 236 minusButton.setOnClickListener(new View.OnClickListener() { 237 @Override 238 public void onClick(View v) { 239 onClickTimeButton(row, tag, false /*down*/, rowIndex); 240 tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 241 } 242 }); 243 244 plusButton.setOnClickListener(new View.OnClickListener() { 245 @Override 246 public void onClick(View v) { 247 onClickTimeButton(row, tag, true /*up*/, rowIndex); 248 tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 249 } 250 }); 251 minusButton.setVisibility(View.VISIBLE); 252 plusButton.setVisibility(View.VISIBLE); 253 254 minusButton.setEnabled(time > MIN_BUCKET_MINUTES); 255 plusButton.setEnabled(tag.countdownZenDuration != MAX_BUCKET_MINUTES); 256 257 minusButton.setAlpha(minusButton.isEnabled() ? 1f : .5f); 258 plusButton.setAlpha(plusButton.isEnabled() ? 1f : .5f); 259 } else { 260 if (minusButton != null) { 261 ((ViewGroup) row).removeView(minusButton); 262 } 263 if (plusButton != null) { 264 ((ViewGroup) row).removeView(plusButton); 265 } 266 } 267 } 268 269 @VisibleForTesting updateUi(ConditionTag tag, View row, int rowIndex)270 protected void updateUi(ConditionTag tag, View row, int rowIndex) { 271 if (tag.lines == null) { 272 setupUi(tag, row); 273 } 274 275 updateButtons(tag, row, rowIndex); 276 277 String radioContentText = ""; 278 switch (rowIndex) { 279 case FOREVER_CONDITION_INDEX: 280 radioContentText = mContext.getString(R.string.zen_mode_forever); 281 break; 282 case COUNTDOWN_CONDITION_INDEX: 283 Condition condition = ZenModeConfig.toTimeCondition(mContext, 284 tag.countdownZenDuration, ActivityManager.getCurrentUser(), false); 285 radioContentText = condition.line1; 286 break; 287 case ALWAYS_ASK_CONDITION_INDEX: 288 radioContentText = mContext.getString( 289 R.string.zen_mode_duration_always_prompt_title); 290 break; 291 } 292 293 tag.line1.setText(radioContentText); 294 } 295 296 @VisibleForTesting onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId)297 protected void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) { 298 int newDndTimeDuration = -1; 299 final int N = MINUTE_BUCKETS.length; 300 if (mBucketIndex == -1) { 301 // not on a known index, search for the next or prev bucket by time 302 final long time = tag.countdownZenDuration; 303 for (int i = 0; i < N; i++) { 304 int j = up ? i : N - 1 - i; 305 final int bucketMinutes = MINUTE_BUCKETS[j]; 306 if (up && bucketMinutes > time || !up && bucketMinutes < time) { 307 mBucketIndex = j; 308 newDndTimeDuration = bucketMinutes; 309 break; 310 } 311 } 312 if (newDndTimeDuration == -1) { 313 mBucketIndex = DEFAULT_BUCKET_INDEX; 314 newDndTimeDuration = MINUTE_BUCKETS[mBucketIndex]; 315 } 316 } else { 317 // on a known index, simply increment or decrement 318 mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1))); 319 newDndTimeDuration = MINUTE_BUCKETS[mBucketIndex]; 320 } 321 tag.countdownZenDuration = newDndTimeDuration; 322 bindTag(newDndTimeDuration, row, rowId); 323 tag.rb.setChecked(true); 324 } 325 326 // used as the view tag on condition rows 327 @VisibleForTesting 328 protected static class ConditionTag { 329 public RadioButton rb; 330 public View lines; 331 public TextView line1; 332 public int countdownZenDuration; // only important for countdown radio button 333 } 334 } 335