1 /*
2  * Copyright (C) 2017 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 android.widget;
18 
19 import android.content.Context;
20 import android.os.LocaleList;
21 import android.text.Editable;
22 import android.text.InputFilter;
23 import android.text.TextUtils;
24 import android.text.TextWatcher;
25 import android.util.AttributeSet;
26 import android.util.MathUtils;
27 import android.view.View;
28 import android.view.accessibility.AccessibilityManager;
29 
30 import com.android.internal.R;
31 
32 /**
33  * View to show text input based time picker with hour and minute fields and an optional AM/PM
34  * spinner.
35  *
36  * @hide
37  */
38 public class TextInputTimePickerView extends RelativeLayout {
39     public static final int HOURS = 0;
40     public static final int MINUTES = 1;
41     public static final int AMPM = 2;
42 
43     private static final int AM = 0;
44     private static final int PM = 1;
45 
46     private final EditText mHourEditText;
47     private final EditText mMinuteEditText;
48     private final TextView mInputSeparatorView;
49     private final Spinner mAmPmSpinner;
50     private final TextView mErrorLabel;
51     private final TextView mHourLabel;
52     private final TextView mMinuteLabel;
53 
54     private boolean mIs24Hour;
55     private boolean mHourFormatStartsAtZero;
56     private OnValueTypedListener mListener;
57 
58     private boolean mErrorShowing;
59     private boolean mTimeSet;
60 
61     interface OnValueTypedListener {
onValueChanged(int inputType, int newValue)62         void onValueChanged(int inputType, int newValue);
63     }
64 
TextInputTimePickerView(Context context)65     public TextInputTimePickerView(Context context) {
66         this(context, null);
67     }
68 
TextInputTimePickerView(Context context, AttributeSet attrs)69     public TextInputTimePickerView(Context context, AttributeSet attrs) {
70         this(context, attrs, 0);
71     }
72 
TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle)73     public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) {
74         this(context, attrs, defStyle, 0);
75     }
76 
TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle, int defStyleRes)77     public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle,
78             int defStyleRes) {
79         super(context, attrs, defStyle, defStyleRes);
80 
81         inflate(context, R.layout.time_picker_text_input_material, this);
82 
83         mHourEditText = findViewById(R.id.input_hour);
84         mMinuteEditText = findViewById(R.id.input_minute);
85         mInputSeparatorView = findViewById(R.id.input_separator);
86         mErrorLabel = findViewById(R.id.label_error);
87         mHourLabel = findViewById(R.id.label_hour);
88         mMinuteLabel = findViewById(R.id.label_minute);
89 
90         mHourEditText.addTextChangedListener(new TextWatcher() {
91             @Override
92             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
93 
94             @Override
95             public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
96 
97             @Override
98             public void afterTextChanged(Editable editable) {
99                 if (parseAndSetHourInternal(editable.toString()) && editable.length() > 1) {
100                     AccessibilityManager am = (AccessibilityManager) context.getSystemService(
101                             context.ACCESSIBILITY_SERVICE);
102                     if (!am.isEnabled()) {
103                         mMinuteEditText.requestFocus();
104                     }
105                 }
106             }
107         });
108 
109         mMinuteEditText.addTextChangedListener(new TextWatcher() {
110             @Override
111             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
112 
113             @Override
114             public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
115 
116             @Override
117             public void afterTextChanged(Editable editable) {
118                 parseAndSetMinuteInternal(editable.toString());
119             }
120         });
121 
122         mAmPmSpinner = findViewById(R.id.am_pm_spinner);
123         final String[] amPmStrings = TimePicker.getAmPmStrings(context);
124         ArrayAdapter<CharSequence> adapter =
125                 new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_dropdown_item);
126         adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[0]));
127         adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[1]));
128         mAmPmSpinner.setAdapter(adapter);
129         mAmPmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
130             @Override
131             public void onItemSelected(AdapterView<?> adapterView, View view, int position,
132                     long id) {
133                 if (position == 0) {
134                     mListener.onValueChanged(AMPM, AM);
135                 } else {
136                     mListener.onValueChanged(AMPM, PM);
137                 }
138             }
139 
140             @Override
141             public void onNothingSelected(AdapterView<?> adapterView) {}
142         });
143     }
144 
setListener(OnValueTypedListener listener)145     void setListener(OnValueTypedListener listener) {
146         mListener = listener;
147     }
148 
setHourFormat(int maxCharLength)149     void setHourFormat(int maxCharLength) {
150         mHourEditText.setFilters(new InputFilter[] {
151                 new InputFilter.LengthFilter(maxCharLength)});
152         mMinuteEditText.setFilters(new InputFilter[] {
153                 new InputFilter.LengthFilter(maxCharLength)});
154         final LocaleList locales = mContext.getResources().getConfiguration().getLocales();
155         mHourEditText.setImeHintLocales(locales);
156         mMinuteEditText.setImeHintLocales(locales);
157     }
158 
validateInput()159     boolean validateInput() {
160         final String hourText = TextUtils.isEmpty(mHourEditText.getText())
161                 ? mHourEditText.getHint().toString()
162                 : mHourEditText.getText().toString();
163         final String minuteText = TextUtils.isEmpty(mMinuteEditText.getText())
164                 ? mMinuteEditText.getHint().toString()
165                 : mMinuteEditText.getText().toString();
166 
167         final boolean inputValid = parseAndSetHourInternal(hourText)
168                 && parseAndSetMinuteInternal(minuteText);
169         setError(!inputValid);
170         return inputValid;
171     }
172 
updateSeparator(String separatorText)173     void updateSeparator(String separatorText) {
174         mInputSeparatorView.setText(separatorText);
175     }
176 
setError(boolean enabled)177     private void setError(boolean enabled) {
178         mErrorShowing = enabled;
179 
180         mErrorLabel.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
181         mHourLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
182         mMinuteLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
183     }
184 
setTimeSet(boolean timeSet)185     private void setTimeSet(boolean timeSet) {
186         mTimeSet = mTimeSet || timeSet;
187     }
188 
isTimeSet()189     private boolean isTimeSet() {
190         return mTimeSet;
191     }
192 
193     /**
194      * Computes the display value and updates the text of the view.
195      * <p>
196      * This method should be called whenever the current value or display
197      * properties (leading zeroes, max digits) change.
198      */
updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour, boolean hourFormatStartsAtZero)199     void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour,
200             boolean hourFormatStartsAtZero) {
201         final String hourFormat = "%d";
202         final String minuteFormat = "%02d";
203 
204         mIs24Hour = is24Hour;
205         mHourFormatStartsAtZero = hourFormatStartsAtZero;
206 
207         mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE);
208 
209         if (amOrPm == AM) {
210             mAmPmSpinner.setSelection(0);
211         } else {
212             mAmPmSpinner.setSelection(1);
213         }
214 
215         if (isTimeSet()) {
216             mHourEditText.setText(String.format(hourFormat, localizedHour));
217             mMinuteEditText.setText(String.format(minuteFormat, minute));
218         } else {
219             mHourEditText.setHint(String.format(hourFormat, localizedHour));
220             mMinuteEditText.setHint(String.format(minuteFormat, minute));
221         }
222 
223 
224         if (mErrorShowing) {
225             validateInput();
226         }
227     }
228 
parseAndSetHourInternal(String input)229     private boolean parseAndSetHourInternal(String input) {
230         try {
231             final int hour = Integer.parseInt(input);
232             if (!isValidLocalizedHour(hour)) {
233                 final int minHour = mHourFormatStartsAtZero ? 0 : 1;
234                 final int maxHour = mIs24Hour ? 23 : 11 + minHour;
235                 mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(
236                         MathUtils.constrain(hour, minHour, maxHour)));
237                 return false;
238             }
239             mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour));
240             setTimeSet(true);
241             return true;
242         } catch (NumberFormatException e) {
243             // Do nothing since we cannot parse the input.
244             return false;
245         }
246     }
247 
parseAndSetMinuteInternal(String input)248     private boolean parseAndSetMinuteInternal(String input) {
249         try {
250             final int minutes = Integer.parseInt(input);
251             if (minutes < 0 || minutes > 59) {
252                 mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59));
253                 return false;
254             }
255             mListener.onValueChanged(MINUTES, minutes);
256             setTimeSet(true);
257             return true;
258         } catch (NumberFormatException e) {
259             // Do nothing since we cannot parse the input.
260             return false;
261         }
262     }
263 
isValidLocalizedHour(int localizedHour)264     private boolean isValidLocalizedHour(int localizedHour) {
265         final int minHour = mHourFormatStartsAtZero ? 0 : 1;
266         final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
267         return localizedHour >= minHour && localizedHour <= maxHour;
268     }
269 
getHourOfDayFromLocalizedHour(int localizedHour)270     private int getHourOfDayFromLocalizedHour(int localizedHour) {
271         int hourOfDay = localizedHour;
272         if (mIs24Hour) {
273             if (!mHourFormatStartsAtZero && localizedHour == 24) {
274                 hourOfDay = 0;
275             }
276         } else {
277             if (!mHourFormatStartsAtZero && localizedHour == 12) {
278                 hourOfDay = 0;
279             }
280             if (mAmPmSpinner.getSelectedItemPosition() == 1) {
281                 hourOfDay += 12;
282             }
283         }
284         return hourOfDay;
285     }
286 }
287