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