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