1 /*
2  * Copyright (C) 2008 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.deskclock.timer;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.Resources;
22 import android.graphics.PorterDuff;
23 import androidx.annotation.IdRes;
24 import androidx.core.view.ViewCompat;
25 import android.text.BidiFormatter;
26 import android.text.TextUtils;
27 import android.text.format.DateUtils;
28 import android.text.style.RelativeSizeSpan;
29 import android.util.AttributeSet;
30 import android.view.KeyEvent;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.widget.LinearLayout;
34 import android.widget.TextView;
35 
36 import com.android.deskclock.FabContainer;
37 import com.android.deskclock.FormattedTextUtils;
38 import com.android.deskclock.R;
39 import com.android.deskclock.ThemeUtils;
40 import com.android.deskclock.uidata.UiDataModel;
41 
42 import java.io.Serializable;
43 import java.util.Arrays;
44 
45 import static com.android.deskclock.FabContainer.FAB_REQUEST_FOCUS;
46 import static com.android.deskclock.FabContainer.FAB_SHRINK_AND_EXPAND;
47 
48 public class TimerSetupView extends LinearLayout implements View.OnClickListener,
49         View.OnLongClickListener {
50 
51     private final int[] mInput = { 0, 0, 0, 0, 0, 0 };
52 
53     private int mInputPointer = -1;
54     private CharSequence mTimeTemplate;
55 
56     private TextView mTimeView;
57     private View mDeleteView;
58     private View mDividerView;
59     private TextView[] mDigitViews;
60 
61     /** Updates to the fab are requested via this container. */
62     private FabContainer mFabContainer;
63 
TimerSetupView(Context context)64     public TimerSetupView(Context context) {
65         this(context, null /* attrs */);
66     }
67 
TimerSetupView(Context context, AttributeSet attrs)68     public TimerSetupView(Context context, AttributeSet attrs) {
69         super(context, attrs);
70 
71         final BidiFormatter bf = BidiFormatter.getInstance(false /* rtlContext */);
72         final String hoursLabel = bf.unicodeWrap(context.getString(R.string.hours_label));
73         final String minutesLabel = bf.unicodeWrap(context.getString(R.string.minutes_label));
74         final String secondsLabel = bf.unicodeWrap(context.getString(R.string.seconds_label));
75 
76         // Create a formatted template for "00h 00m 00s".
77         mTimeTemplate = TextUtils.expandTemplate("^1^4 ^2^5 ^3^6",
78                 bf.unicodeWrap("^1"),
79                 bf.unicodeWrap("^2"),
80                 bf.unicodeWrap("^3"),
81                 FormattedTextUtils.formatText(hoursLabel, new RelativeSizeSpan(0.5f)),
82                 FormattedTextUtils.formatText(minutesLabel, new RelativeSizeSpan(0.5f)),
83                 FormattedTextUtils.formatText(secondsLabel, new RelativeSizeSpan(0.5f)));
84 
85         LayoutInflater.from(context).inflate(R.layout.timer_setup_container, this);
86     }
87 
88     @Override
onFinishInflate()89     protected void onFinishInflate() {
90         super.onFinishInflate();
91 
92         mTimeView = (TextView) findViewById(R.id.timer_setup_time);
93         mDeleteView = findViewById(R.id.timer_setup_delete);
94         mDividerView = findViewById(R.id.timer_setup_divider);
95         mDigitViews = new TextView[] {
96                 (TextView) findViewById(R.id.timer_setup_digit_0),
97                 (TextView) findViewById(R.id.timer_setup_digit_1),
98                 (TextView) findViewById(R.id.timer_setup_digit_2),
99                 (TextView) findViewById(R.id.timer_setup_digit_3),
100                 (TextView) findViewById(R.id.timer_setup_digit_4),
101                 (TextView) findViewById(R.id.timer_setup_digit_5),
102                 (TextView) findViewById(R.id.timer_setup_digit_6),
103                 (TextView) findViewById(R.id.timer_setup_digit_7),
104                 (TextView) findViewById(R.id.timer_setup_digit_8),
105                 (TextView) findViewById(R.id.timer_setup_digit_9),
106         };
107 
108         // Tint the divider to match the disabled control color by default and used the activated
109         // control color when there is valid input.
110         final Context dividerContext = mDividerView.getContext();
111         final int colorControlActivated = ThemeUtils.resolveColor(dividerContext,
112                 R.attr.colorControlActivated);
113         final int colorControlDisabled = ThemeUtils.resolveColor(dividerContext,
114                 R.attr.colorControlNormal, new int[] { ~android.R.attr.state_enabled });
115         ViewCompat.setBackgroundTintList(mDividerView, new ColorStateList(
116                 new int[][] { { android.R.attr.state_activated }, {} },
117                 new int[] { colorControlActivated, colorControlDisabled }));
118         ViewCompat.setBackgroundTintMode(mDividerView, PorterDuff.Mode.SRC);
119 
120         // Initialize the digit buttons.
121         final UiDataModel uidm = UiDataModel.getUiDataModel();
122         for (final TextView digitView : mDigitViews) {
123             final int digit = getDigitForId(digitView.getId());
124             digitView.setText(uidm.getFormattedNumber(digit, 1));
125             digitView.setOnClickListener(this);
126         }
127 
128         mDeleteView.setOnClickListener(this);
129         mDeleteView.setOnLongClickListener(this);
130 
131         updateTime();
132         updateDeleteAndDivider();
133     }
134 
setFabContainer(FabContainer fabContainer)135     public void setFabContainer(FabContainer fabContainer) {
136         mFabContainer = fabContainer;
137     }
138 
139     @Override
onKeyDown(int keyCode, KeyEvent event)140     public boolean onKeyDown(int keyCode, KeyEvent event) {
141         View view = null;
142         if (keyCode == KeyEvent.KEYCODE_DEL) {
143             view = mDeleteView;
144         } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
145             view = mDigitViews[keyCode - KeyEvent.KEYCODE_0];
146         }
147 
148         if (view != null) {
149             final boolean result = view.performClick();
150             if (result && hasValidInput()) {
151                 mFabContainer.updateFab(FAB_REQUEST_FOCUS);
152             }
153             return result;
154         }
155 
156         return false;
157     }
158 
159     @Override
onClick(View view)160     public void onClick(View view) {
161         if (view == mDeleteView) {
162             delete();
163         } else {
164             append(getDigitForId(view.getId()));
165         }
166     }
167 
168     @Override
onLongClick(View view)169     public boolean onLongClick(View view) {
170         if (view == mDeleteView) {
171             reset();
172             updateFab();
173             return true;
174         }
175         return false;
176     }
177 
getDigitForId(@dRes int id)178     private int getDigitForId(@IdRes int id) {
179         switch (id) {
180             case R.id.timer_setup_digit_0:
181                 return 0;
182             case R.id.timer_setup_digit_1:
183                 return 1;
184             case R.id.timer_setup_digit_2:
185                 return 2;
186             case R.id.timer_setup_digit_3:
187                 return 3;
188             case R.id.timer_setup_digit_4:
189                 return 4;
190             case R.id.timer_setup_digit_5:
191                 return 5;
192             case R.id.timer_setup_digit_6:
193                 return 6;
194             case R.id.timer_setup_digit_7:
195                 return 7;
196             case R.id.timer_setup_digit_8:
197                 return 8;
198             case R.id.timer_setup_digit_9:
199                 return 9;
200         }
201         throw new IllegalArgumentException("Invalid id: " + id);
202     }
203 
updateTime()204     private void updateTime() {
205         final int seconds = mInput[1] * 10 + mInput[0];
206         final int minutes = mInput[3] * 10 + mInput[2];
207         final int hours = mInput[5] * 10 + mInput[4];
208 
209         final UiDataModel uidm = UiDataModel.getUiDataModel();
210         mTimeView.setText(TextUtils.expandTemplate(mTimeTemplate,
211                 uidm.getFormattedNumber(hours, 2),
212                 uidm.getFormattedNumber(minutes, 2),
213                 uidm.getFormattedNumber(seconds, 2)));
214 
215         final Resources r = getResources();
216         mTimeView.setContentDescription(r.getString(R.string.timer_setup_description,
217                 r.getQuantityString(R.plurals.hours, hours, hours),
218                 r.getQuantityString(R.plurals.minutes, minutes, minutes),
219                 r.getQuantityString(R.plurals.seconds, seconds, seconds)));
220     }
221 
updateDeleteAndDivider()222     private void updateDeleteAndDivider() {
223         final boolean enabled = hasValidInput();
224         mDeleteView.setEnabled(enabled);
225         mDividerView.setActivated(enabled);
226     }
227 
updateFab()228     private void updateFab() {
229         mFabContainer.updateFab(FAB_SHRINK_AND_EXPAND);
230     }
231 
append(int digit)232     private void append(int digit) {
233         if (digit < 0 || digit > 9) {
234             throw new IllegalArgumentException("Invalid digit: " + digit);
235         }
236 
237         // Pressing "0" as the first digit does nothing.
238         if (mInputPointer == -1 && digit == 0) {
239             return;
240         }
241 
242         // No space for more digits, so ignore input.
243         if (mInputPointer == mInput.length - 1) {
244             return;
245         }
246 
247         // Append the new digit.
248         System.arraycopy(mInput, 0, mInput, 1, mInputPointer + 1);
249         mInput[0] = digit;
250         mInputPointer++;
251         updateTime();
252 
253         // Update TalkBack to read the number being deleted.
254         mDeleteView.setContentDescription(getContext().getString(
255                 R.string.timer_descriptive_delete,
256                 UiDataModel.getUiDataModel().getFormattedNumber(digit)));
257 
258         // Update the fab, delete, and divider when we have valid input.
259         if (mInputPointer == 0) {
260             updateFab();
261             updateDeleteAndDivider();
262         }
263     }
264 
delete()265     private void delete() {
266         // Nothing exists to delete so return.
267         if (mInputPointer < 0) {
268             return;
269         }
270 
271         System.arraycopy(mInput, 1, mInput, 0, mInputPointer);
272         mInput[mInputPointer] = 0;
273         mInputPointer--;
274         updateTime();
275 
276         // Update TalkBack to read the number being deleted or its original description.
277         if (mInputPointer >= 0) {
278             mDeleteView.setContentDescription(getContext().getString(
279                     R.string.timer_descriptive_delete,
280                     UiDataModel.getUiDataModel().getFormattedNumber(mInput[0])));
281         } else {
282             mDeleteView.setContentDescription(getContext().getString(R.string.timer_delete));
283         }
284 
285         // Update the fab, delete, and divider when we no longer have valid input.
286         if (mInputPointer == -1) {
287             updateFab();
288             updateDeleteAndDivider();
289         }
290     }
291 
reset()292     public void reset() {
293         if (mInputPointer != -1) {
294             Arrays.fill(mInput, 0);
295             mInputPointer = -1;
296             updateTime();
297             updateDeleteAndDivider();
298         }
299     }
300 
hasValidInput()301     public boolean hasValidInput() {
302         return mInputPointer != -1;
303     }
304 
getTimeInMillis()305     public long getTimeInMillis() {
306         final int seconds = mInput[1] * 10 + mInput[0];
307         final int minutes = mInput[3] * 10 + mInput[2];
308         final int hours = mInput[5] * 10 + mInput[4];
309         return seconds * DateUtils.SECOND_IN_MILLIS
310                 + minutes * DateUtils.MINUTE_IN_MILLIS
311                 + hours * DateUtils.HOUR_IN_MILLIS;
312     }
313 
314     /**
315      * @return an opaque representation of the state of timer setup
316      */
getState()317     public Serializable getState() {
318         return Arrays.copyOf(mInput, mInput.length);
319     }
320 
321     /**
322      * @param state an opaque state of this view previously produced by {@link #getState()}
323      */
setState(Serializable state)324     public void setState(Serializable state) {
325         final int[] input = (int[]) state;
326         if (input != null && mInput.length == input.length) {
327             for (int i = 0; i < mInput.length; i++) {
328                 mInput[i] = input[i];
329                 if (mInput[i] != 0) {
330                     mInputPointer = i;
331                 }
332             }
333             updateTime();
334             updateDeleteAndDivider();
335         }
336     }
337 }
338