| /* | 
 |  * Copyright (C) 2017 The Android Open Source Project | 
 |  * | 
 |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
 |  * you may not use this file except in compliance with the License. | 
 |  * You may obtain a copy of the License at | 
 |  * | 
 |  *      http://www.apache.org/licenses/LICENSE-2.0 | 
 |  * | 
 |  * Unless required by applicable law or agreed to in writing, software | 
 |  * distributed under the License is distributed on an "AS IS" BASIS, | 
 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 |  * See the License for the specific language governing permissions and | 
 |  * limitations under the License. | 
 |  */ | 
 |  | 
 | package android.widget; | 
 |  | 
 | import android.content.Context; | 
 | import android.os.LocaleList; | 
 | import android.text.Editable; | 
 | import android.text.InputFilter; | 
 | import android.text.TextUtils; | 
 | import android.text.TextWatcher; | 
 | import android.util.AttributeSet; | 
 | import android.util.MathUtils; | 
 | import android.view.View; | 
 | import android.view.accessibility.AccessibilityManager; | 
 |  | 
 | import com.android.internal.R; | 
 |  | 
 | /** | 
 |  * View to show text input based time picker with hour and minute fields and an optional AM/PM | 
 |  * spinner. | 
 |  * | 
 |  * @hide | 
 |  */ | 
 | public class TextInputTimePickerView extends RelativeLayout { | 
 |     public static final int HOURS = 0; | 
 |     public static final int MINUTES = 1; | 
 |     public static final int AMPM = 2; | 
 |  | 
 |     private static final int AM = 0; | 
 |     private static final int PM = 1; | 
 |  | 
 |     private final EditText mHourEditText; | 
 |     private final EditText mMinuteEditText; | 
 |     private final TextView mInputSeparatorView; | 
 |     private final Spinner mAmPmSpinner; | 
 |     private final TextView mErrorLabel; | 
 |     private final TextView mHourLabel; | 
 |     private final TextView mMinuteLabel; | 
 |  | 
 |     private boolean mIs24Hour; | 
 |     private boolean mHourFormatStartsAtZero; | 
 |     private OnValueTypedListener mListener; | 
 |  | 
 |     private boolean mErrorShowing; | 
 |     private boolean mTimeSet; | 
 |  | 
 |     interface OnValueTypedListener { | 
 |         void onValueChanged(int inputType, int newValue); | 
 |     } | 
 |  | 
 |     public TextInputTimePickerView(Context context) { | 
 |         this(context, null); | 
 |     } | 
 |  | 
 |     public TextInputTimePickerView(Context context, AttributeSet attrs) { | 
 |         this(context, attrs, 0); | 
 |     } | 
 |  | 
 |     public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) { | 
 |         this(context, attrs, defStyle, 0); | 
 |     } | 
 |  | 
 |     public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle, | 
 |             int defStyleRes) { | 
 |         super(context, attrs, defStyle, defStyleRes); | 
 |  | 
 |         inflate(context, R.layout.time_picker_text_input_material, this); | 
 |  | 
 |         mHourEditText = findViewById(R.id.input_hour); | 
 |         mMinuteEditText = findViewById(R.id.input_minute); | 
 |         mInputSeparatorView = findViewById(R.id.input_separator); | 
 |         mErrorLabel = findViewById(R.id.label_error); | 
 |         mHourLabel = findViewById(R.id.label_hour); | 
 |         mMinuteLabel = findViewById(R.id.label_minute); | 
 |  | 
 |         mHourEditText.addTextChangedListener(new TextWatcher() { | 
 |             @Override | 
 |             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} | 
 |  | 
 |             @Override | 
 |             public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} | 
 |  | 
 |             @Override | 
 |             public void afterTextChanged(Editable editable) { | 
 |                 if (parseAndSetHourInternal(editable.toString()) && editable.length() > 1) { | 
 |                     AccessibilityManager am = (AccessibilityManager) context.getSystemService( | 
 |                             context.ACCESSIBILITY_SERVICE); | 
 |                     if (!am.isEnabled()) { | 
 |                         mMinuteEditText.requestFocus(); | 
 |                     } | 
 |                 } | 
 |             } | 
 |         }); | 
 |  | 
 |         mMinuteEditText.addTextChangedListener(new TextWatcher() { | 
 |             @Override | 
 |             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} | 
 |  | 
 |             @Override | 
 |             public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} | 
 |  | 
 |             @Override | 
 |             public void afterTextChanged(Editable editable) { | 
 |                 parseAndSetMinuteInternal(editable.toString()); | 
 |             } | 
 |         }); | 
 |  | 
 |         mAmPmSpinner = findViewById(R.id.am_pm_spinner); | 
 |         final String[] amPmStrings = TimePicker.getAmPmStrings(context); | 
 |         ArrayAdapter<CharSequence> adapter = | 
 |                 new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_dropdown_item); | 
 |         adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[0])); | 
 |         adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[1])); | 
 |         mAmPmSpinner.setAdapter(adapter); | 
 |         mAmPmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { | 
 |             @Override | 
 |             public void onItemSelected(AdapterView<?> adapterView, View view, int position, | 
 |                     long id) { | 
 |                 if (position == 0) { | 
 |                     mListener.onValueChanged(AMPM, AM); | 
 |                 } else { | 
 |                     mListener.onValueChanged(AMPM, PM); | 
 |                 } | 
 |             } | 
 |  | 
 |             @Override | 
 |             public void onNothingSelected(AdapterView<?> adapterView) {} | 
 |         }); | 
 |     } | 
 |  | 
 |     void setListener(OnValueTypedListener listener) { | 
 |         mListener = listener; | 
 |     } | 
 |  | 
 |     void setHourFormat(int maxCharLength) { | 
 |         mHourEditText.setFilters(new InputFilter[] { | 
 |                 new InputFilter.LengthFilter(maxCharLength)}); | 
 |         mMinuteEditText.setFilters(new InputFilter[] { | 
 |                 new InputFilter.LengthFilter(maxCharLength)}); | 
 |         final LocaleList locales = mContext.getResources().getConfiguration().getLocales(); | 
 |         mHourEditText.setImeHintLocales(locales); | 
 |         mMinuteEditText.setImeHintLocales(locales); | 
 |     } | 
 |  | 
 |     boolean validateInput() { | 
 |         final String hourText = TextUtils.isEmpty(mHourEditText.getText()) | 
 |                 ? mHourEditText.getHint().toString() | 
 |                 : mHourEditText.getText().toString(); | 
 |         final String minuteText = TextUtils.isEmpty(mMinuteEditText.getText()) | 
 |                 ? mMinuteEditText.getHint().toString() | 
 |                 : mMinuteEditText.getText().toString(); | 
 |  | 
 |         final boolean inputValid = parseAndSetHourInternal(hourText) | 
 |                 && parseAndSetMinuteInternal(minuteText); | 
 |         setError(!inputValid); | 
 |         return inputValid; | 
 |     } | 
 |  | 
 |     void updateSeparator(String separatorText) { | 
 |         mInputSeparatorView.setText(separatorText); | 
 |     } | 
 |  | 
 |     private void setError(boolean enabled) { | 
 |         mErrorShowing = enabled; | 
 |  | 
 |         mErrorLabel.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); | 
 |         mHourLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE); | 
 |         mMinuteLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE); | 
 |     } | 
 |  | 
 |     private void setTimeSet(boolean timeSet) { | 
 |         mTimeSet = mTimeSet || timeSet; | 
 |     } | 
 |  | 
 |     private boolean isTimeSet() { | 
 |         return mTimeSet; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Computes the display value and updates the text of the view. | 
 |      * <p> | 
 |      * This method should be called whenever the current value or display | 
 |      * properties (leading zeroes, max digits) change. | 
 |      */ | 
 |     void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour, | 
 |             boolean hourFormatStartsAtZero) { | 
 |         final String hourFormat = "%d"; | 
 |         final String minuteFormat = "%02d"; | 
 |  | 
 |         mIs24Hour = is24Hour; | 
 |         mHourFormatStartsAtZero = hourFormatStartsAtZero; | 
 |  | 
 |         mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE); | 
 |  | 
 |         if (amOrPm == AM) { | 
 |             mAmPmSpinner.setSelection(0); | 
 |         } else { | 
 |             mAmPmSpinner.setSelection(1); | 
 |         } | 
 |  | 
 |         if (isTimeSet()) { | 
 |             mHourEditText.setText(String.format(hourFormat, localizedHour)); | 
 |             mMinuteEditText.setText(String.format(minuteFormat, minute)); | 
 |         } else { | 
 |             mHourEditText.setHint(String.format(hourFormat, localizedHour)); | 
 |             mMinuteEditText.setHint(String.format(minuteFormat, minute)); | 
 |         } | 
 |  | 
 |  | 
 |         if (mErrorShowing) { | 
 |             validateInput(); | 
 |         } | 
 |     } | 
 |  | 
 |     private boolean parseAndSetHourInternal(String input) { | 
 |         try { | 
 |             final int hour = Integer.parseInt(input); | 
 |             if (!isValidLocalizedHour(hour)) { | 
 |                 final int minHour = mHourFormatStartsAtZero ? 0 : 1; | 
 |                 final int maxHour = mIs24Hour ? 23 : 11 + minHour; | 
 |                 mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour( | 
 |                         MathUtils.constrain(hour, minHour, maxHour))); | 
 |                 return false; | 
 |             } | 
 |             mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour)); | 
 |             setTimeSet(true); | 
 |             return true; | 
 |         } catch (NumberFormatException e) { | 
 |             // Do nothing since we cannot parse the input. | 
 |             return false; | 
 |         } | 
 |     } | 
 |  | 
 |     private boolean parseAndSetMinuteInternal(String input) { | 
 |         try { | 
 |             final int minutes = Integer.parseInt(input); | 
 |             if (minutes < 0 || minutes > 59) { | 
 |                 mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59)); | 
 |                 return false; | 
 |             } | 
 |             mListener.onValueChanged(MINUTES, minutes); | 
 |             setTimeSet(true); | 
 |             return true; | 
 |         } catch (NumberFormatException e) { | 
 |             // Do nothing since we cannot parse the input. | 
 |             return false; | 
 |         } | 
 |     } | 
 |  | 
 |     private boolean isValidLocalizedHour(int localizedHour) { | 
 |         final int minHour = mHourFormatStartsAtZero ? 0 : 1; | 
 |         final int maxHour = (mIs24Hour ? 23 : 11) + minHour; | 
 |         return localizedHour >= minHour && localizedHour <= maxHour; | 
 |     } | 
 |  | 
 |     private int getHourOfDayFromLocalizedHour(int localizedHour) { | 
 |         int hourOfDay = localizedHour; | 
 |         if (mIs24Hour) { | 
 |             if (!mHourFormatStartsAtZero && localizedHour == 24) { | 
 |                 hourOfDay = 0; | 
 |             } | 
 |         } else { | 
 |             if (!mHourFormatStartsAtZero && localizedHour == 12) { | 
 |                 hourOfDay = 0; | 
 |             } | 
 |             if (mAmPmSpinner.getSelectedItemPosition() == 1) { | 
 |                 hourOfDay += 12; | 
 |             } | 
 |         } | 
 |         return hourOfDay; | 
 |     } | 
 | } |