TimePickerClockDelegate.java revision 3053b2fdcf7486f2e2f572f9b05ce65dacdd2b4c
1/*
2 * Copyright (C) 2013 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
17package android.widget;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.content.res.TypedArray;
22import android.graphics.Color;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.text.format.DateFormat;
26import android.text.format.DateUtils;
27import android.util.AttributeSet;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.accessibility.AccessibilityEvent;
32import android.view.accessibility.AccessibilityNodeInfo;
33import android.view.inputmethod.EditorInfo;
34import android.view.inputmethod.InputMethodManager;
35import com.android.internal.R;
36
37import java.text.DateFormatSymbols;
38import java.util.Calendar;
39import java.util.Locale;
40
41import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
42import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
43
44/**
45 * A delegate implementing the basic TimePicker
46 */
47class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
48
49    private static final boolean DEFAULT_ENABLED_STATE = true;
50
51    private static final int HOURS_IN_HALF_DAY = 12;
52
53    // state
54    private boolean mIs24HourView;
55
56    private boolean mIsAm;
57
58    // ui components
59    private final NumberPicker mHourSpinner;
60
61    private final NumberPicker mMinuteSpinner;
62
63    private final NumberPicker mAmPmSpinner;
64
65    private final EditText mHourSpinnerInput;
66
67    private final EditText mMinuteSpinnerInput;
68
69    private final EditText mAmPmSpinnerInput;
70
71    private final TextView mDivider;
72
73    // Note that the legacy implementation of the TimePicker is
74    // using a button for toggling between AM/PM while the new
75    // version uses a NumberPicker spinner. Therefore the code
76    // accommodates these two cases to be backwards compatible.
77    private final Button mAmPmButton;
78
79    // May be null if layout has no done button
80    private final View mDoneButton;
81    private boolean mShowDoneButton;
82    private TimePicker.TimePickerDismissCallback mDismissCallback;
83
84    private final String[] mAmPmStrings;
85
86    private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
87
88    private Calendar mTempCalendar;
89
90    private boolean mHourWithTwoDigit;
91    private char mHourFormat;
92
93    /**
94     * A no-op callback used in the constructor to avoid null checks later in
95     * the code.
96     */
97    private static final TimePicker.OnTimeChangedListener NO_OP_CHANGE_LISTENER =
98            new TimePicker.OnTimeChangedListener() {
99                public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
100                }
101            };
102
103    public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs,
104            int defStyleAttr, int defStyleRes) {
105        super(delegator, context);
106
107        // process style attributes
108        final TypedArray a = mContext.obtainStyledAttributes(
109                attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
110        final int layoutResourceId = a.getResourceId(
111                R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
112        a.recycle();
113
114        final LayoutInflater inflater = LayoutInflater.from(mContext);
115        inflater.inflate(layoutResourceId, mDelegator, true);
116
117        // hour
118        mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour);
119        mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
120            public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
121                updateInputState();
122                if (!is24HourView()) {
123                    if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
124                            (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
125                        mIsAm = !mIsAm;
126                        updateAmPmControl();
127                    }
128                }
129                onTimeChanged();
130            }
131        });
132        mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
133        mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
134
135        // divider (only for the new widget style)
136        mDivider = (TextView) mDelegator.findViewById(R.id.divider);
137        if (mDivider != null) {
138            setDividerText();
139        }
140
141        // minute
142        mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute);
143        mMinuteSpinner.setMinValue(0);
144        mMinuteSpinner.setMaxValue(59);
145        mMinuteSpinner.setOnLongPressUpdateInterval(100);
146        mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
147        mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
148            public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
149                updateInputState();
150                int minValue = mMinuteSpinner.getMinValue();
151                int maxValue = mMinuteSpinner.getMaxValue();
152                if (oldVal == maxValue && newVal == minValue) {
153                    int newHour = mHourSpinner.getValue() + 1;
154                    if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) {
155                        mIsAm = !mIsAm;
156                        updateAmPmControl();
157                    }
158                    mHourSpinner.setValue(newHour);
159                } else if (oldVal == minValue && newVal == maxValue) {
160                    int newHour = mHourSpinner.getValue() - 1;
161                    if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) {
162                        mIsAm = !mIsAm;
163                        updateAmPmControl();
164                    }
165                    mHourSpinner.setValue(newHour);
166                }
167                onTimeChanged();
168            }
169        });
170        mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
171        mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
172
173            /* Get the localized am/pm strings and use them in the spinner */
174        mAmPmStrings = new DateFormatSymbols().getAmPmStrings();
175
176        // am/pm
177        View amPmView = mDelegator.findViewById(R.id.amPm);
178        if (amPmView instanceof Button) {
179            mAmPmSpinner = null;
180            mAmPmSpinnerInput = null;
181            mAmPmButton = (Button) amPmView;
182            mAmPmButton.setOnClickListener(new View.OnClickListener() {
183                public void onClick(View button) {
184                    button.requestFocus();
185                    mIsAm = !mIsAm;
186                    updateAmPmControl();
187                    onTimeChanged();
188                }
189            });
190        } else {
191            mAmPmButton = null;
192            mAmPmSpinner = (NumberPicker) amPmView;
193            mAmPmSpinner.setMinValue(0);
194            mAmPmSpinner.setMaxValue(1);
195            mAmPmSpinner.setDisplayedValues(mAmPmStrings);
196            mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
197                public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
198                    updateInputState();
199                    picker.requestFocus();
200                    mIsAm = !mIsAm;
201                    updateAmPmControl();
202                    onTimeChanged();
203                }
204            });
205            mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
206            mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
207        }
208
209        if (isAmPmAtStart()) {
210            // Move the am/pm view to the beginning
211            ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout);
212            amPmParent.removeView(amPmView);
213            amPmParent.addView(amPmView, 0);
214            // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
215            // for example and not for Holo Theme)
216            ViewGroup.MarginLayoutParams lp =
217                    (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
218            final int startMargin = lp.getMarginStart();
219            final int endMargin = lp.getMarginEnd();
220            if (startMargin != endMargin) {
221                lp.setMarginStart(endMargin);
222                lp.setMarginEnd(startMargin);
223            }
224        }
225
226        mDoneButton = delegator.findViewById(R.id.done_button);
227        mShowDoneButton = (mDoneButton != null);
228        if (mShowDoneButton) {
229            mDoneButton.setOnClickListener(new View.OnClickListener() {
230                @Override
231                public void onClick(View v) {
232                    if (mDismissCallback != null) {
233                        mDismissCallback.dismiss(mDelegator, false, getCurrentHour(),
234                                                 getCurrentMinute());
235                    }
236                }
237            });
238        }
239
240        getHourFormatData();
241
242        // update controls to initial state
243        updateHourControl();
244        updateMinuteControl();
245        updateAmPmControl();
246
247        setOnTimeChangedListener(NO_OP_CHANGE_LISTENER);
248
249        // set to current time
250        setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
251        setCurrentMinute(mTempCalendar.get(Calendar.MINUTE));
252
253        if (!isEnabled()) {
254            setEnabled(false);
255        }
256
257        // set the content descriptions
258        setContentDescriptions();
259
260        // If not explicitly specified this view is important for accessibility.
261        if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
262            mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
263        }
264
265        updateDoneButton();
266    }
267
268    private void getHourFormatData() {
269        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
270                (mIs24HourView) ? "Hm" : "hm");
271        final int lengthPattern = bestDateTimePattern.length();
272        mHourWithTwoDigit = false;
273        char hourFormat = '\0';
274        // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
275        // the hour format that we found.
276        for (int i = 0; i < lengthPattern; i++) {
277            final char c = bestDateTimePattern.charAt(i);
278            if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
279                mHourFormat = c;
280                if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
281                    mHourWithTwoDigit = true;
282                }
283                break;
284            }
285        }
286    }
287
288    private boolean isAmPmAtStart() {
289        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
290                "hm" /* skeleton */);
291
292        return bestDateTimePattern.startsWith("a");
293    }
294
295    /**
296     * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
297     *
298     * See http://unicode.org/cldr/trac/browser/trunk/common/main
299     *
300     * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
301     * separator as the character which is just after the hour marker in the returned pattern.
302     */
303    private void setDividerText() {
304        final String skeleton = (mIs24HourView) ? "Hm" : "hm";
305        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
306                skeleton);
307        final String separatorText;
308        int hourIndex = bestDateTimePattern.lastIndexOf('H');
309        if (hourIndex == -1) {
310            hourIndex = bestDateTimePattern.lastIndexOf('h');
311        }
312        if (hourIndex == -1) {
313            // Default case
314            separatorText = ":";
315        } else {
316            int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
317            if  (minuteIndex == -1) {
318                separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
319            } else {
320                separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
321            }
322        }
323        mDivider.setText(separatorText);
324    }
325
326    @Override
327    public void setCurrentHour(Integer currentHour) {
328        setCurrentHour(currentHour, true);
329    }
330
331    private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) {
332        // why was Integer used in the first place?
333        if (currentHour == null || currentHour == getCurrentHour()) {
334            return;
335        }
336        if (!is24HourView()) {
337            // convert [0,23] ordinal to wall clock display
338            if (currentHour >= HOURS_IN_HALF_DAY) {
339                mIsAm = false;
340                if (currentHour > HOURS_IN_HALF_DAY) {
341                    currentHour = currentHour - HOURS_IN_HALF_DAY;
342                }
343            } else {
344                mIsAm = true;
345                if (currentHour == 0) {
346                    currentHour = HOURS_IN_HALF_DAY;
347                }
348            }
349            updateAmPmControl();
350        }
351        mHourSpinner.setValue(currentHour);
352        if (notifyTimeChanged) {
353            onTimeChanged();
354        }
355    }
356
357    @Override
358    public Integer getCurrentHour() {
359        int currentHour = mHourSpinner.getValue();
360        if (is24HourView()) {
361            return currentHour;
362        } else if (mIsAm) {
363            return currentHour % HOURS_IN_HALF_DAY;
364        } else {
365            return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
366        }
367    }
368
369    @Override
370    public void setCurrentMinute(Integer currentMinute) {
371        if (currentMinute == getCurrentMinute()) {
372            return;
373        }
374        mMinuteSpinner.setValue(currentMinute);
375        onTimeChanged();
376    }
377
378    @Override
379    public Integer getCurrentMinute() {
380        return mMinuteSpinner.getValue();
381    }
382
383    @Override
384    public void setIs24HourView(Boolean is24HourView) {
385        if (mIs24HourView == is24HourView) {
386            return;
387        }
388        // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
389        int currentHour = getCurrentHour();
390        // Order is important here.
391        mIs24HourView = is24HourView;
392        getHourFormatData();
393        updateHourControl();
394        // set value after spinner range is updated
395        setCurrentHour(currentHour, false);
396        updateMinuteControl();
397        updateAmPmControl();
398    }
399
400    @Override
401    public boolean is24HourView() {
402        return mIs24HourView;
403    }
404
405    @Override
406    public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) {
407        mOnTimeChangedListener = onTimeChangedListener;
408    }
409
410    @Override
411    public void setEnabled(boolean enabled) {
412        mMinuteSpinner.setEnabled(enabled);
413        if (mDivider != null) {
414            mDivider.setEnabled(enabled);
415        }
416        mHourSpinner.setEnabled(enabled);
417        if (mAmPmSpinner != null) {
418            mAmPmSpinner.setEnabled(enabled);
419        } else {
420            mAmPmButton.setEnabled(enabled);
421        }
422        mIsEnabled = enabled;
423    }
424
425    @Override
426    public boolean isEnabled() {
427        return mIsEnabled;
428    }
429
430    @Override
431    public void setShowDoneButton(boolean showDoneButton) {
432        if (mDoneButton != null) {
433            mShowDoneButton = showDoneButton;
434            updateDoneButton();
435        }
436    }
437
438    @Override
439    public boolean isShowDoneButton() {
440        return mShowDoneButton;
441    }
442
443    private void updateDoneButton() {
444        if (mDoneButton != null) {
445            mDoneButton.setVisibility(mShowDoneButton ? View.VISIBLE : View.GONE);
446        }
447    }
448
449    @Override
450    public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) {
451        mDismissCallback = callback;
452    }
453
454    @Override
455    public int getBaseline() {
456        return mHourSpinner.getBaseline();
457    }
458
459    @Override
460    public void onConfigurationChanged(Configuration newConfig) {
461        setCurrentLocale(newConfig.locale);
462    }
463
464    @Override
465    public Parcelable onSaveInstanceState(Parcelable superState) {
466        return new SavedState(superState, getCurrentHour(), getCurrentMinute(),
467                isShowDoneButton());
468    }
469
470    @Override
471    public void onRestoreInstanceState(Parcelable state) {
472        SavedState ss = (SavedState) state;
473        setCurrentHour(ss.getHour());
474        setCurrentMinute(ss.getMinute());
475        setShowDoneButton(ss.isShowDoneButton());
476    }
477
478    @Override
479    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
480        onPopulateAccessibilityEvent(event);
481        return true;
482    }
483
484    @Override
485    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
486        int flags = DateUtils.FORMAT_SHOW_TIME;
487        if (mIs24HourView) {
488            flags |= DateUtils.FORMAT_24HOUR;
489        } else {
490            flags |= DateUtils.FORMAT_12HOUR;
491        }
492        mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
493        mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
494        String selectedDateUtterance = DateUtils.formatDateTime(mContext,
495                mTempCalendar.getTimeInMillis(), flags);
496        event.getText().add(selectedDateUtterance);
497    }
498
499    @Override
500    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
501        event.setClassName(TimePicker.class.getName());
502    }
503
504    @Override
505    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
506        info.setClassName(TimePicker.class.getName());
507    }
508
509    private void updateInputState() {
510        // Make sure that if the user changes the value and the IME is active
511        // for one of the inputs if this widget, the IME is closed. If the user
512        // changed the value via the IME and there is a next input the IME will
513        // be shown, otherwise the user chose another means of changing the
514        // value and having the IME up makes no sense.
515        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
516        if (inputMethodManager != null) {
517            if (inputMethodManager.isActive(mHourSpinnerInput)) {
518                mHourSpinnerInput.clearFocus();
519                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
520            } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
521                mMinuteSpinnerInput.clearFocus();
522                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
523            } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
524                mAmPmSpinnerInput.clearFocus();
525                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
526            }
527        }
528    }
529
530    private void updateAmPmControl() {
531        if (is24HourView()) {
532            if (mAmPmSpinner != null) {
533                mAmPmSpinner.setVisibility(View.GONE);
534            } else {
535                mAmPmButton.setVisibility(View.GONE);
536            }
537        } else {
538            int index = mIsAm ? Calendar.AM : Calendar.PM;
539            if (mAmPmSpinner != null) {
540                mAmPmSpinner.setValue(index);
541                mAmPmSpinner.setVisibility(View.VISIBLE);
542            } else {
543                mAmPmButton.setText(mAmPmStrings[index]);
544                mAmPmButton.setVisibility(View.VISIBLE);
545            }
546        }
547        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
548    }
549
550    /**
551     * Sets the current locale.
552     *
553     * @param locale The current locale.
554     */
555    @Override
556    public void setCurrentLocale(Locale locale) {
557        super.setCurrentLocale(locale);
558        mTempCalendar = Calendar.getInstance(locale);
559    }
560
561    private void onTimeChanged() {
562        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
563        if (mOnTimeChangedListener != null) {
564            mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(),
565                    getCurrentMinute());
566        }
567    }
568
569    private void updateHourControl() {
570        if (is24HourView()) {
571            // 'k' means 1-24 hour
572            if (mHourFormat == 'k') {
573                mHourSpinner.setMinValue(1);
574                mHourSpinner.setMaxValue(24);
575            } else {
576                mHourSpinner.setMinValue(0);
577                mHourSpinner.setMaxValue(23);
578            }
579        } else {
580            // 'K' means 0-11 hour
581            if (mHourFormat == 'K') {
582                mHourSpinner.setMinValue(0);
583                mHourSpinner.setMaxValue(11);
584            } else {
585                mHourSpinner.setMinValue(1);
586                mHourSpinner.setMaxValue(12);
587            }
588        }
589        mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
590    }
591
592    private void updateMinuteControl() {
593        if (is24HourView()) {
594            mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
595        } else {
596            mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
597        }
598    }
599
600    private void setContentDescriptions() {
601        // Minute
602        trySetContentDescription(mMinuteSpinner, R.id.increment,
603                R.string.time_picker_increment_minute_button);
604        trySetContentDescription(mMinuteSpinner, R.id.decrement,
605                R.string.time_picker_decrement_minute_button);
606        // Hour
607        trySetContentDescription(mHourSpinner, R.id.increment,
608                R.string.time_picker_increment_hour_button);
609        trySetContentDescription(mHourSpinner, R.id.decrement,
610                R.string.time_picker_decrement_hour_button);
611        // AM/PM
612        if (mAmPmSpinner != null) {
613            trySetContentDescription(mAmPmSpinner, R.id.increment,
614                    R.string.time_picker_increment_set_pm_button);
615            trySetContentDescription(mAmPmSpinner, R.id.decrement,
616                    R.string.time_picker_decrement_set_am_button);
617        }
618    }
619
620    private void trySetContentDescription(View root, int viewId, int contDescResId) {
621        View target = root.findViewById(viewId);
622        if (target != null) {
623            target.setContentDescription(mContext.getString(contDescResId));
624        }
625    }
626
627    /**
628     * Used to save / restore state of time picker
629     */
630    private static class SavedState extends View.BaseSavedState {
631
632        private final int mHour;
633
634        private final int mMinute;
635
636        private final boolean mShowDoneButton;
637
638        private SavedState(Parcelable superState, int hour, int minute, boolean showDoneButton) {
639            super(superState);
640            mHour = hour;
641            mMinute = minute;
642            mShowDoneButton = showDoneButton;
643        }
644
645        private SavedState(Parcel in) {
646            super(in);
647            mHour = in.readInt();
648            mMinute = in.readInt();
649            mShowDoneButton = (in.readInt() == 1);
650        }
651
652        public int getHour() {
653            return mHour;
654        }
655
656        public int getMinute() {
657            return mMinute;
658        }
659
660        public boolean isShowDoneButton() {
661            return mShowDoneButton;
662        }
663
664        @Override
665        public void writeToParcel(Parcel dest, int flags) {
666            super.writeToParcel(dest, flags);
667            dest.writeInt(mHour);
668            dest.writeInt(mMinute);
669            dest.writeInt(mShowDoneButton ? 1 : 0);
670        }
671
672        @SuppressWarnings({"unused", "hiding"})
673        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
674            public SavedState createFromParcel(Parcel in) {
675                return new SavedState(in);
676            }
677
678            public SavedState[] newArray(int size) {
679                return new SavedState[size];
680            }
681        };
682    }
683}
684
685