1 /*
2  * Copyright (C) 2016 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.compat.annotation.UnsupportedAppUsage;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.content.res.TypedArray;
23 import android.icu.util.Calendar;
24 import android.os.Build;
25 import android.os.Parcelable;
26 import android.text.InputType;
27 import android.text.TextUtils;
28 import android.text.format.DateFormat;
29 import android.util.AttributeSet;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.accessibility.AccessibilityEvent;
33 import android.view.inputmethod.EditorInfo;
34 import android.view.inputmethod.InputMethodManager;
35 import android.widget.DatePicker.AbstractDatePickerDelegate;
36 import android.widget.NumberPicker.OnValueChangeListener;
37 
38 import java.text.DateFormatSymbols;
39 import java.text.ParseException;
40 import java.text.SimpleDateFormat;
41 import java.util.Arrays;
42 import java.util.Locale;
43 
44 /**
45  * A delegate implementing the basic DatePicker
46  */
47 class DatePickerSpinnerDelegate extends AbstractDatePickerDelegate {
48 
49     private static final String DATE_FORMAT = "MM/dd/yyyy";
50 
51     private static final int DEFAULT_START_YEAR = 1900;
52 
53     private static final int DEFAULT_END_YEAR = 2100;
54 
55     private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
56 
57     private static final boolean DEFAULT_SPINNERS_SHOWN = true;
58 
59     private static final boolean DEFAULT_ENABLED_STATE = true;
60 
61     private final LinearLayout mSpinners;
62 
63     private final NumberPicker mDaySpinner;
64 
65     private final NumberPicker mMonthSpinner;
66 
67     private final NumberPicker mYearSpinner;
68 
69     private final EditText mDaySpinnerInput;
70 
71     private final EditText mMonthSpinnerInput;
72 
73     private final EditText mYearSpinnerInput;
74 
75     private final CalendarView mCalendarView;
76 
77     private String[] mShortMonths;
78 
79     private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
80 
81     private int mNumberOfMonths;
82 
83     private Calendar mTempDate;
84 
85     private Calendar mMinDate;
86 
87     private Calendar mMaxDate;
88 
89     private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
90 
DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)91     DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs,
92             int defStyleAttr, int defStyleRes) {
93         super(delegator, context);
94 
95         mDelegator = delegator;
96         mContext = context;
97 
98         // initialization based on locale
99         setCurrentLocale(Locale.getDefault());
100 
101         final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
102                 com.android.internal.R.styleable.DatePicker, defStyleAttr, defStyleRes);
103         boolean spinnersShown = attributesArray.getBoolean(com.android.internal.R.styleable.DatePicker_spinnersShown,
104                 DEFAULT_SPINNERS_SHOWN);
105         boolean calendarViewShown = attributesArray.getBoolean(
106                 com.android.internal.R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
107         int startYear = attributesArray.getInt(com.android.internal.R.styleable.DatePicker_startYear,
108                 DEFAULT_START_YEAR);
109         int endYear = attributesArray.getInt(com.android.internal.R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
110         String minDate = attributesArray.getString(com.android.internal.R.styleable.DatePicker_minDate);
111         String maxDate = attributesArray.getString(com.android.internal.R.styleable.DatePicker_maxDate);
112         int layoutResourceId = attributesArray.getResourceId(
113                 com.android.internal.R.styleable.DatePicker_legacyLayout, com.android.internal.R.layout.date_picker_legacy);
114         attributesArray.recycle();
115 
116         LayoutInflater inflater = (LayoutInflater) context
117                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
118         final View view = inflater.inflate(layoutResourceId, mDelegator, true);
119         view.setSaveFromParentEnabled(false);
120 
121         OnValueChangeListener onChangeListener = new OnValueChangeListener() {
122             public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
123                 updateInputState();
124                 mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
125                 // take care of wrapping of days and months to update greater fields
126                 if (picker == mDaySpinner) {
127                     int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
128                     if (oldVal == maxDayOfMonth && newVal == 1) {
129                         mTempDate.add(Calendar.DAY_OF_MONTH, 1);
130                     } else if (oldVal == 1 && newVal == maxDayOfMonth) {
131                         mTempDate.add(Calendar.DAY_OF_MONTH, -1);
132                     } else {
133                         mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
134                     }
135                 } else if (picker == mMonthSpinner) {
136                     if (oldVal == 11 && newVal == 0) {
137                         mTempDate.add(Calendar.MONTH, 1);
138                     } else if (oldVal == 0 && newVal == 11) {
139                         mTempDate.add(Calendar.MONTH, -1);
140                     } else {
141                         mTempDate.add(Calendar.MONTH, newVal - oldVal);
142                     }
143                 } else if (picker == mYearSpinner) {
144                     mTempDate.set(Calendar.YEAR, newVal);
145                 } else {
146                     throw new IllegalArgumentException();
147                 }
148                 // now set the date to the adjusted one
149                 setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
150                         mTempDate.get(Calendar.DAY_OF_MONTH));
151                 updateSpinners();
152                 updateCalendarView();
153                 notifyDateChanged();
154             }
155         };
156 
157         mSpinners = (LinearLayout) mDelegator.findViewById(com.android.internal.R.id.pickers);
158 
159         // calendar view day-picker
160         mCalendarView = (CalendarView) mDelegator.findViewById(com.android.internal.R.id.calendar_view);
161         mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
162             public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
163                 setDate(year, month, monthDay);
164                 updateSpinners();
165                 notifyDateChanged();
166             }
167         });
168 
169         // day
170         mDaySpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.day);
171         mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
172         mDaySpinner.setOnLongPressUpdateInterval(100);
173         mDaySpinner.setOnValueChangedListener(onChangeListener);
174         mDaySpinnerInput = (EditText) mDaySpinner.findViewById(com.android.internal.R.id.numberpicker_input);
175 
176         // month
177         mMonthSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.month);
178         mMonthSpinner.setMinValue(0);
179         mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
180         mMonthSpinner.setDisplayedValues(mShortMonths);
181         mMonthSpinner.setOnLongPressUpdateInterval(200);
182         mMonthSpinner.setOnValueChangedListener(onChangeListener);
183         mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(com.android.internal.R.id.numberpicker_input);
184 
185         // year
186         mYearSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.year);
187         mYearSpinner.setOnLongPressUpdateInterval(100);
188         mYearSpinner.setOnValueChangedListener(onChangeListener);
189         mYearSpinnerInput = (EditText) mYearSpinner.findViewById(com.android.internal.R.id.numberpicker_input);
190 
191         // show only what the user required but make sure we
192         // show something and the spinners have higher priority
193         if (!spinnersShown && !calendarViewShown) {
194             setSpinnersShown(true);
195         } else {
196             setSpinnersShown(spinnersShown);
197             setCalendarViewShown(calendarViewShown);
198         }
199 
200         // set the min date giving priority of the minDate over startYear
201         mTempDate.clear();
202         if (!TextUtils.isEmpty(minDate)) {
203             if (!parseDate(minDate, mTempDate)) {
204                 mTempDate.set(startYear, 0, 1);
205             }
206         } else {
207             mTempDate.set(startYear, 0, 1);
208         }
209         setMinDate(mTempDate.getTimeInMillis());
210 
211         // set the max date giving priority of the maxDate over endYear
212         mTempDate.clear();
213         if (!TextUtils.isEmpty(maxDate)) {
214             if (!parseDate(maxDate, mTempDate)) {
215                 mTempDate.set(endYear, 11, 31);
216             }
217         } else {
218             mTempDate.set(endYear, 11, 31);
219         }
220         setMaxDate(mTempDate.getTimeInMillis());
221 
222         // initialize to current date
223         mCurrentDate.setTimeInMillis(System.currentTimeMillis());
224         init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
225                 .get(Calendar.DAY_OF_MONTH), null);
226 
227         // re-order the number spinners to match the current date format
228         reorderSpinners();
229 
230         // accessibility
231         setContentDescriptions();
232 
233         // If not explicitly specified this view is important for accessibility.
234         if (mDelegator.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
235             mDelegator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
236         }
237     }
238 
239     @Override
init(int year, int monthOfYear, int dayOfMonth, DatePicker.OnDateChangedListener onDateChangedListener)240     public void init(int year, int monthOfYear, int dayOfMonth,
241                      DatePicker.OnDateChangedListener onDateChangedListener) {
242         setDate(year, monthOfYear, dayOfMonth);
243         updateSpinners();
244         updateCalendarView();
245 
246         mOnDateChangedListener = onDateChangedListener;
247     }
248 
249     @Override
updateDate(int year, int month, int dayOfMonth)250     public void updateDate(int year, int month, int dayOfMonth) {
251         if (!isNewDate(year, month, dayOfMonth)) {
252             return;
253         }
254         setDate(year, month, dayOfMonth);
255         updateSpinners();
256         updateCalendarView();
257         notifyDateChanged();
258     }
259 
260     @Override
getYear()261     public int getYear() {
262         return mCurrentDate.get(Calendar.YEAR);
263     }
264 
265     @Override
getMonth()266     public int getMonth() {
267         return mCurrentDate.get(Calendar.MONTH);
268     }
269 
270     @Override
getDayOfMonth()271     public int getDayOfMonth() {
272         return mCurrentDate.get(Calendar.DAY_OF_MONTH);
273     }
274 
275     @Override
setFirstDayOfWeek(int firstDayOfWeek)276     public void setFirstDayOfWeek(int firstDayOfWeek) {
277         mCalendarView.setFirstDayOfWeek(firstDayOfWeek);
278     }
279 
280     @Override
getFirstDayOfWeek()281     public int getFirstDayOfWeek() {
282         return mCalendarView.getFirstDayOfWeek();
283     }
284 
285     @Override
setMinDate(long minDate)286     public void setMinDate(long minDate) {
287         mTempDate.setTimeInMillis(minDate);
288         if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
289                 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
290             // Same day, no-op.
291             return;
292         }
293         mMinDate.setTimeInMillis(minDate);
294         mCalendarView.setMinDate(minDate);
295         if (mCurrentDate.before(mMinDate)) {
296             mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
297             updateCalendarView();
298         }
299         updateSpinners();
300     }
301 
302     @Override
getMinDate()303     public Calendar getMinDate() {
304         final Calendar minDate = Calendar.getInstance();
305         minDate.setTimeInMillis(mCalendarView.getMinDate());
306         return minDate;
307     }
308 
309     @Override
setMaxDate(long maxDate)310     public void setMaxDate(long maxDate) {
311         mTempDate.setTimeInMillis(maxDate);
312         if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
313                 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
314             // Same day, no-op.
315             return;
316         }
317         mMaxDate.setTimeInMillis(maxDate);
318         mCalendarView.setMaxDate(maxDate);
319         if (mCurrentDate.after(mMaxDate)) {
320             mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
321             updateCalendarView();
322         }
323         updateSpinners();
324     }
325 
326     @Override
getMaxDate()327     public Calendar getMaxDate() {
328         final Calendar maxDate = Calendar.getInstance();
329         maxDate.setTimeInMillis(mCalendarView.getMaxDate());
330         return maxDate;
331     }
332 
333     @Override
setEnabled(boolean enabled)334     public void setEnabled(boolean enabled) {
335         mDaySpinner.setEnabled(enabled);
336         mMonthSpinner.setEnabled(enabled);
337         mYearSpinner.setEnabled(enabled);
338         mCalendarView.setEnabled(enabled);
339         mIsEnabled = enabled;
340     }
341 
342     @Override
isEnabled()343     public boolean isEnabled() {
344         return mIsEnabled;
345     }
346 
347     @Override
getCalendarView()348     public CalendarView getCalendarView() {
349         return mCalendarView;
350     }
351 
352     @Override
setCalendarViewShown(boolean shown)353     public void setCalendarViewShown(boolean shown) {
354         mCalendarView.setVisibility(shown ? View.VISIBLE : View.GONE);
355     }
356 
357     @Override
getCalendarViewShown()358     public boolean getCalendarViewShown() {
359         return (mCalendarView.getVisibility() == View.VISIBLE);
360     }
361 
362     @Override
setSpinnersShown(boolean shown)363     public void setSpinnersShown(boolean shown) {
364         mSpinners.setVisibility(shown ? View.VISIBLE : View.GONE);
365     }
366 
367     @Override
getSpinnersShown()368     public boolean getSpinnersShown() {
369         return mSpinners.isShown();
370     }
371 
372     @Override
onConfigurationChanged(Configuration newConfig)373     public void onConfigurationChanged(Configuration newConfig) {
374         setCurrentLocale(newConfig.locale);
375     }
376 
377     @Override
onSaveInstanceState(Parcelable superState)378     public Parcelable onSaveInstanceState(Parcelable superState) {
379         return new SavedState(superState, getYear(), getMonth(), getDayOfMonth(),
380                 getMinDate().getTimeInMillis(), getMaxDate().getTimeInMillis());
381     }
382 
383     @Override
onRestoreInstanceState(Parcelable state)384     public void onRestoreInstanceState(Parcelable state) {
385         if (state instanceof SavedState) {
386             final SavedState ss = (SavedState) state;
387             setDate(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
388             updateSpinners();
389             updateCalendarView();
390         }
391     }
392 
393     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)394     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
395         onPopulateAccessibilityEvent(event);
396         return true;
397     }
398 
399     /**
400      * Sets the current locale.
401      *
402      * @param locale The current locale.
403      */
404     @Override
setCurrentLocale(Locale locale)405     protected void setCurrentLocale(Locale locale) {
406         super.setCurrentLocale(locale);
407 
408         mTempDate = getCalendarForLocale(mTempDate, locale);
409         mMinDate = getCalendarForLocale(mMinDate, locale);
410         mMaxDate = getCalendarForLocale(mMaxDate, locale);
411         mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
412 
413         mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
414         mShortMonths = new DateFormatSymbols().getShortMonths();
415 
416         if (usingNumericMonths()) {
417             // We're in a locale where a date should either be all-numeric, or all-text.
418             // All-text would require custom NumberPicker formatters for day and year.
419             mShortMonths = new String[mNumberOfMonths];
420             for (int i = 0; i < mNumberOfMonths; ++i) {
421                 mShortMonths[i] = String.format("%d", i + 1);
422             }
423         }
424     }
425 
426     /**
427      * Tests whether the current locale is one where there are no real month names,
428      * such as Chinese, Japanese, or Korean locales.
429      */
usingNumericMonths()430     private boolean usingNumericMonths() {
431         return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0));
432     }
433 
434     /**
435      * Gets a calendar for locale bootstrapped with the value of a given calendar.
436      *
437      * @param oldCalendar The old calendar.
438      * @param locale The locale.
439      */
getCalendarForLocale(Calendar oldCalendar, Locale locale)440     private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
441         if (oldCalendar == null) {
442             return Calendar.getInstance(locale);
443         } else {
444             final long currentTimeMillis = oldCalendar.getTimeInMillis();
445             Calendar newCalendar = Calendar.getInstance(locale);
446             newCalendar.setTimeInMillis(currentTimeMillis);
447             return newCalendar;
448         }
449     }
450 
451     /**
452      * Reorders the spinners according to the date format that is
453      * explicitly set by the user and if no such is set fall back
454      * to the current locale's default format.
455      */
reorderSpinners()456     private void reorderSpinners() {
457         mSpinners.removeAllViews();
458         // We use numeric spinners for year and day, but textual months. Ask icu4c what
459         // order the user's locale uses for that combination. http://b/7207103.
460         String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd");
461         char[] order = DateFormat.getDateFormatOrder(pattern);
462         final int spinnerCount = order.length;
463         for (int i = 0; i < spinnerCount; i++) {
464             switch (order[i]) {
465                 case 'd':
466                     mSpinners.addView(mDaySpinner);
467                     setImeOptions(mDaySpinner, spinnerCount, i);
468                     break;
469                 case 'M':
470                     mSpinners.addView(mMonthSpinner);
471                     setImeOptions(mMonthSpinner, spinnerCount, i);
472                     break;
473                 case 'y':
474                     mSpinners.addView(mYearSpinner);
475                     setImeOptions(mYearSpinner, spinnerCount, i);
476                     break;
477                 default:
478                     throw new IllegalArgumentException(Arrays.toString(order));
479             }
480         }
481     }
482 
483     /**
484      * Parses the given <code>date</code> and in case of success sets the result
485      * to the <code>outDate</code>.
486      *
487      * @return True if the date was parsed.
488      */
parseDate(String date, Calendar outDate)489     private boolean parseDate(String date, Calendar outDate) {
490         try {
491             outDate.setTime(mDateFormat.parse(date));
492             return true;
493         } catch (ParseException e) {
494             e.printStackTrace();
495             return false;
496         }
497     }
498 
isNewDate(int year, int month, int dayOfMonth)499     private boolean isNewDate(int year, int month, int dayOfMonth) {
500         return (mCurrentDate.get(Calendar.YEAR) != year
501                 || mCurrentDate.get(Calendar.MONTH) != month
502                 || mCurrentDate.get(Calendar.DAY_OF_MONTH) != dayOfMonth);
503     }
504 
505     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
setDate(int year, int month, int dayOfMonth)506     private void setDate(int year, int month, int dayOfMonth) {
507         mCurrentDate.set(year, month, dayOfMonth);
508         resetAutofilledValue();
509         if (mCurrentDate.before(mMinDate)) {
510             mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
511         } else if (mCurrentDate.after(mMaxDate)) {
512             mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
513         }
514     }
515 
516     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
updateSpinners()517     private void updateSpinners() {
518         // set the spinner ranges respecting the min and max dates
519         if (mCurrentDate.equals(mMinDate)) {
520             mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
521             mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
522             mDaySpinner.setWrapSelectorWheel(false);
523             mMonthSpinner.setDisplayedValues(null);
524             mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
525             mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
526             mMonthSpinner.setWrapSelectorWheel(false);
527         } else if (mCurrentDate.equals(mMaxDate)) {
528             mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
529             mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
530             mDaySpinner.setWrapSelectorWheel(false);
531             mMonthSpinner.setDisplayedValues(null);
532             mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
533             mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
534             mMonthSpinner.setWrapSelectorWheel(false);
535         } else {
536             mDaySpinner.setMinValue(1);
537             mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
538             mDaySpinner.setWrapSelectorWheel(true);
539             mMonthSpinner.setDisplayedValues(null);
540             mMonthSpinner.setMinValue(0);
541             mMonthSpinner.setMaxValue(11);
542             mMonthSpinner.setWrapSelectorWheel(true);
543         }
544 
545         // make sure the month names are a zero based array
546         // with the months in the month spinner
547         String[] displayedValues = Arrays.copyOfRange(mShortMonths,
548                 mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
549         mMonthSpinner.setDisplayedValues(displayedValues);
550 
551         // year spinner range does not change based on the current date
552         mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
553         mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
554         mYearSpinner.setWrapSelectorWheel(false);
555 
556         // set the spinner values
557         mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
558         mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
559         mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
560 
561         if (usingNumericMonths()) {
562             mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER);
563         }
564     }
565 
566     /**
567      * Updates the calendar view with the current date.
568      */
569     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
updateCalendarView()570     private void updateCalendarView() {
571         mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
572     }
573 
574 
575     /**
576      * Notifies the listener, if such, for a change in the selected date.
577      */
578     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
notifyDateChanged()579     private void notifyDateChanged() {
580         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
581         if (mOnDateChangedListener != null) {
582             mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(),
583                     getDayOfMonth());
584         }
585         if (mAutoFillChangeListener != null) {
586             mAutoFillChangeListener.onDateChanged(mDelegator, getYear(), getMonth(),
587                     getDayOfMonth());
588         }
589     }
590 
591     /**
592      * Sets the IME options for a spinner based on its ordering.
593      *
594      * @param spinner The spinner.
595      * @param spinnerCount The total spinner count.
596      * @param spinnerIndex The index of the given spinner.
597      */
setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex)598     private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) {
599         final int imeOptions;
600         if (spinnerIndex < spinnerCount - 1) {
601             imeOptions = EditorInfo.IME_ACTION_NEXT;
602         } else {
603             imeOptions = EditorInfo.IME_ACTION_DONE;
604         }
605         TextView input = (TextView) spinner.findViewById(com.android.internal.R.id.numberpicker_input);
606         input.setImeOptions(imeOptions);
607     }
608 
setContentDescriptions()609     private void setContentDescriptions() {
610         // Day
611         trySetContentDescription(mDaySpinner, com.android.internal.R.id.increment,
612                 com.android.internal.R.string.date_picker_increment_day_button);
613         trySetContentDescription(mDaySpinner, com.android.internal.R.id.decrement,
614                 com.android.internal.R.string.date_picker_decrement_day_button);
615         // Month
616         trySetContentDescription(mMonthSpinner, com.android.internal.R.id.increment,
617                 com.android.internal.R.string.date_picker_increment_month_button);
618         trySetContentDescription(mMonthSpinner, com.android.internal.R.id.decrement,
619                 com.android.internal.R.string.date_picker_decrement_month_button);
620         // Year
621         trySetContentDescription(mYearSpinner, com.android.internal.R.id.increment,
622                 com.android.internal.R.string.date_picker_increment_year_button);
623         trySetContentDescription(mYearSpinner, com.android.internal.R.id.decrement,
624                 com.android.internal.R.string.date_picker_decrement_year_button);
625     }
626 
trySetContentDescription(View root, int viewId, int contDescResId)627     private void trySetContentDescription(View root, int viewId, int contDescResId) {
628         View target = root.findViewById(viewId);
629         if (target != null) {
630             target.setContentDescription(mContext.getString(contDescResId));
631         }
632     }
633 
634     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
updateInputState()635     private void updateInputState() {
636         // Make sure that if the user changes the value and the IME is active
637         // for one of the inputs if this widget, the IME is closed. If the user
638         // changed the value via the IME and there is a next input the IME will
639         // be shown, otherwise the user chose another means of changing the
640         // value and having the IME up makes no sense.
641         InputMethodManager inputMethodManager = mContext.getSystemService(InputMethodManager.class);
642         if (inputMethodManager != null) {
643             if (inputMethodManager.isActive(mYearSpinnerInput)) {
644                 mYearSpinnerInput.clearFocus();
645                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
646             } else if (inputMethodManager.isActive(mMonthSpinnerInput)) {
647                 mMonthSpinnerInput.clearFocus();
648                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
649             } else if (inputMethodManager.isActive(mDaySpinnerInput)) {
650                 mDaySpinnerInput.clearFocus();
651                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
652             }
653         }
654     }
655 }
656