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