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