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 com.android.datetimepicker.date;
18
19import android.animation.ObjectAnimator;
20import android.app.Activity;
21import android.app.DialogFragment;
22import android.content.Context;
23import android.content.res.Resources;
24import android.os.Bundle;
25import android.os.SystemClock;
26import android.os.Vibrator;
27import android.text.format.DateFormat;
28import android.text.format.DateUtils;
29import android.text.format.Time;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.View.OnClickListener;
35import android.view.ViewGroup;
36import android.view.Window;
37import android.view.WindowManager;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.animation.AlphaAnimation;
40import android.view.animation.Animation;
41import android.widget.Button;
42import android.widget.LinearLayout;
43import android.widget.TextView;
44import android.widget.ViewAnimator;
45
46import com.android.datetimepicker.R;
47import com.android.datetimepicker.Utils;
48import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay;
49
50import java.text.SimpleDateFormat;
51import java.util.Calendar;
52import java.util.HashSet;
53import java.util.Iterator;
54import java.util.Locale;
55
56/**
57 * Dialog allowing users to select a date.
58 */
59public class DatePickerDialog extends DialogFragment implements
60        OnClickListener, DatePickerController {
61
62    private static final String TAG = "DatePickerDialog";
63
64    private static final int UNINITIALIZED = -1;
65    private static final int MONTH_AND_DAY_VIEW = 0;
66    private static final int YEAR_VIEW = 1;
67
68    private static final String KEY_SELECTED_YEAR = "year";
69    private static final String KEY_SELECTED_MONTH = "month";
70    private static final String KEY_SELECTED_DAY = "day";
71    private static final String KEY_LIST_POSITION = "list_position";
72    private static final String KEY_WEEK_START = "week_start";
73    private static final String KEY_YEAR_START = "year_start";
74    private static final String KEY_YEAR_END = "year_end";
75    private static final String KEY_CURRENT_VIEW = "current_view";
76    private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset";
77
78    private static final int DEFAULT_START_YEAR = 1900;
79    private static final int DEFAULT_END_YEAR = 2100;
80
81    private static final int ANIMATION_DURATION = 300;
82    private static final int ANIMATION_DELAY = 500;
83
84    private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());
85    private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault());
86
87    private final Calendar mCalendar = Calendar.getInstance();
88    private OnDateSetListener mCallBack;
89    private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
90
91    private AccessibleDateAnimator mAnimator;
92
93    private TextView mDayOfWeekView;
94    private LinearLayout mMonthAndDayView;
95    private TextView mSelectedMonthTextView;
96    private TextView mSelectedDayTextView;
97    private TextView mYearView;
98    private DayPickerView mDayPickerView;
99    private YearPickerView mYearPickerView;
100    private Button mDoneButton;
101
102    private int mCurrentView = UNINITIALIZED;
103
104    private int mWeekStart = mCalendar.getFirstDayOfWeek();
105    private int mMinYear = DEFAULT_START_YEAR;
106    private int mMaxYear = DEFAULT_END_YEAR;
107
108    private Vibrator mVibrator;
109    private long mLastVibrate;
110
111    private boolean mDelayAnimation = true;
112
113    // Accessibility strings.
114    private String mDayPickerDescription;
115    private String mSelectDay;
116    private String mYearPickerDescription;
117    private String mSelectYear;
118
119    /**
120     * The callback used to indicate the user is done filling in the date.
121     */
122    public interface OnDateSetListener {
123
124        /**
125         * @param view The view associated with this listener.
126         * @param year The year that was set.
127         * @param monthOfYear The month that was set (0-11) for compatibility
128         *            with {@link java.util.Calendar}.
129         * @param dayOfMonth The day of the month that was set.
130         */
131        void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth);
132    }
133
134    /**
135     * The callback used to notify other date picker components of a change in selected date.
136     */
137    interface OnDateChangedListener {
138
139        public void onDateChanged();
140    }
141
142
143    public DatePickerDialog() {
144        // Empty constructor required for dialog fragment.
145    }
146
147    /**
148     * @param callBack How the parent is notified that the date is set.
149     * @param year The initial year of the dialog.
150     * @param monthOfYear The initial month of the dialog.
151     * @param dayOfMonth The initial day of the dialog.
152     */
153    public static DatePickerDialog newInstance(OnDateSetListener callBack, int year,
154            int monthOfYear,
155            int dayOfMonth) {
156        DatePickerDialog ret = new DatePickerDialog();
157        ret.initialize(callBack, year, monthOfYear, dayOfMonth);
158        return ret;
159    }
160
161    public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
162        mCallBack = callBack;
163        mCalendar.set(Calendar.YEAR, year);
164        mCalendar.set(Calendar.MONTH, monthOfYear);
165        mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
166    }
167
168    @Override
169    public void onCreate(Bundle savedInstanceState) {
170        super.onCreate(savedInstanceState);
171        final Activity activity = getActivity();
172        activity.getWindow().setSoftInputMode(
173                WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
174        mVibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
175        if (savedInstanceState != null) {
176            mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR));
177            mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH));
178            mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY));
179        }
180    }
181
182    @Override
183    public void onSaveInstanceState(Bundle outState) {
184        super.onSaveInstanceState(outState);
185        outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR));
186        outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH));
187        outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH));
188        outState.putInt(KEY_WEEK_START, mWeekStart);
189        outState.putInt(KEY_YEAR_START, mMinYear);
190        outState.putInt(KEY_YEAR_END, mMaxYear);
191        outState.putInt(KEY_CURRENT_VIEW, mCurrentView);
192        int listPosition = -1;
193        if (mCurrentView == MONTH_AND_DAY_VIEW) {
194            listPosition = mDayPickerView.getMostVisiblePosition();
195        } else if (mCurrentView == YEAR_VIEW) {
196            listPosition = mYearPickerView.getFirstVisiblePosition();
197            outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset());
198        }
199        outState.putInt(KEY_LIST_POSITION, listPosition);
200    }
201
202    @Override
203    public View onCreateView(LayoutInflater inflater, ViewGroup container,
204            Bundle savedInstanceState) {
205        Log.d(TAG, "onCreateView: ");
206        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
207
208        View view = inflater.inflate(R.layout.date_picker_dialog, null);
209
210        mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header);
211        mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day);
212        mMonthAndDayView.setOnClickListener(this);
213        mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month);
214        mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day);
215        mYearView = (TextView) view.findViewById(R.id.date_picker_year);
216        mYearView.setOnClickListener(this);
217
218        int listPosition = -1;
219        int listPositionOffset = 0;
220        int currentView = MONTH_AND_DAY_VIEW;
221        if (savedInstanceState != null) {
222            mWeekStart = savedInstanceState.getInt(KEY_WEEK_START);
223            mMinYear = savedInstanceState.getInt(KEY_YEAR_START);
224            mMaxYear = savedInstanceState.getInt(KEY_YEAR_END);
225            currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW);
226            listPosition = savedInstanceState.getInt(KEY_LIST_POSITION);
227            listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET);
228        }
229
230        final Activity activity = getActivity();
231        mDayPickerView = new DayPickerView(activity, this);
232        mYearPickerView = new YearPickerView(activity, this);
233
234        Resources res = getResources();
235        mDayPickerDescription = res.getString(R.string.day_picker_description);
236        mSelectDay = res.getString(R.string.select_day);
237        mYearPickerDescription = res.getString(R.string.year_picker_description);
238        mSelectYear = res.getString(R.string.select_year);
239
240        mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator);
241        mAnimator.addView(mDayPickerView);
242        mAnimator.addView(mYearPickerView);
243        mAnimator.setDateMillis(mCalendar.getTimeInMillis());
244        // TODO: Replace with animation decided upon by the design team.
245        Animation animation = new AlphaAnimation(0.0f, 1.0f);
246        animation.setDuration(ANIMATION_DURATION);
247        mAnimator.setInAnimation(animation);
248        // TODO: Replace with animation decided upon by the design team.
249        Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
250        animation2.setDuration(ANIMATION_DURATION);
251        mAnimator.setOutAnimation(animation2);
252
253        mDoneButton = (Button) view.findViewById(R.id.done);
254        mDoneButton.setOnClickListener(new OnClickListener() {
255
256            @Override
257            public void onClick(View v) {
258                tryVibrate();
259                if (mCallBack != null) {
260                    mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR),
261                            mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH));
262                }
263                dismiss();
264            }
265        });
266
267        updateDisplay(false);
268        setCurrentView(currentView);
269
270        if (listPosition != -1) {
271            if (currentView == MONTH_AND_DAY_VIEW) {
272                mDayPickerView.postSetSelection(listPosition);
273            } else if (currentView == YEAR_VIEW) {
274                mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset);
275            }
276        }
277        return view;
278    }
279
280    private void setCurrentView(final int viewIndex) {
281        long millis = mCalendar.getTimeInMillis();
282
283        switch (viewIndex) {
284            case MONTH_AND_DAY_VIEW:
285                ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f,
286                        1.05f);
287                if (mDelayAnimation) {
288                    pulseAnimator.setStartDelay(ANIMATION_DELAY);
289                    mDelayAnimation = false;
290                }
291                mDayPickerView.onDateChanged();
292                if (mCurrentView != viewIndex) {
293                    mMonthAndDayView.setSelected(true);
294                    mYearView.setSelected(false);
295                    mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
296                    mCurrentView = viewIndex;
297                }
298                pulseAnimator.start();
299
300                int flags = DateUtils.FORMAT_SHOW_DATE;
301                String dayString = DateUtils.formatDateTime(getActivity(), millis, flags);
302                mAnimator.setContentDescription(mDayPickerDescription+": "+dayString);
303                Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay);
304                break;
305            case YEAR_VIEW:
306                pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f);
307                if (mDelayAnimation) {
308                    pulseAnimator.setStartDelay(ANIMATION_DELAY);
309                    mDelayAnimation = false;
310                }
311                mYearPickerView.onDateChanged();
312                if (mCurrentView != viewIndex) {
313                    mMonthAndDayView.setSelected(false);
314                    mYearView.setSelected(true);
315                    mAnimator.setDisplayedChild(YEAR_VIEW);
316                    mCurrentView = viewIndex;
317                }
318                pulseAnimator.start();
319
320                CharSequence yearString = YEAR_FORMAT.format(millis);
321                mAnimator.setContentDescription(mYearPickerDescription+": "+yearString);
322                Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear);
323                break;
324        }
325    }
326
327    private void updateDisplay(boolean announce) {
328        if (mDayOfWeekView != null) {
329            mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
330                    Locale.getDefault()).toUpperCase(Locale.getDefault()));
331        }
332
333        mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT,
334                Locale.getDefault()).toUpperCase(Locale.getDefault()));
335        mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime()));
336        mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime()));
337
338        // Accessibility.
339        long millis = mCalendar.getTimeInMillis();
340        mAnimator.setDateMillis(millis);
341        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
342        String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags);
343        mMonthAndDayView.setContentDescription(monthAndDayText);
344
345        if (announce) {
346            flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
347            String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags);
348            Utils.tryAccessibilityAnnounce(mAnimator, fullDateText);
349        }
350    }
351
352    public void setFirstDayOfWeek(int startOfWeek) {
353        if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) {
354            throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " +
355                    "Calendar.SATURDAY");
356        }
357        mWeekStart = startOfWeek;
358        if (mDayPickerView != null) {
359            mDayPickerView.onChange();
360        }
361    }
362
363    public void setYearRange(int startYear, int endYear) {
364        if (endYear <= startYear) {
365            throw new IllegalArgumentException("Year end must be larger than year start");
366        }
367        mMinYear = startYear;
368        mMaxYear = endYear;
369        if (mDayPickerView != null) {
370            mDayPickerView.onChange();
371        }
372    }
373
374    public void setOnDateSetListener(OnDateSetListener listener) {
375        mCallBack = listener;
376    }
377
378    // If the newly selected month / year does not contain the currently selected day number,
379    // change the selected day number to the last day of the selected month or year.
380    //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
381    //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
382    private void adjustDayInMonthIfNeeded(int month, int year) {
383        int day = mCalendar.get(Calendar.DAY_OF_MONTH);
384        int daysInMonth = Utils.getDaysInMonth(month, year);
385        if (day > daysInMonth) {
386            mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth);
387        }
388    }
389
390    @Override
391    public void onClick(View v) {
392        tryVibrate();
393        if (v.getId() == R.id.date_picker_year) {
394            setCurrentView(YEAR_VIEW);
395        } else if (v.getId() == R.id.date_picker_month_and_day) {
396            setCurrentView(MONTH_AND_DAY_VIEW);
397        }
398    }
399
400    @Override
401    public void onYearSelected(int year) {
402        adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year);
403        mCalendar.set(Calendar.YEAR, year);
404        updatePickers();
405        setCurrentView(MONTH_AND_DAY_VIEW);
406        updateDisplay(true);
407    }
408
409    @Override
410    public void onDayOfMonthSelected(int year, int month, int day) {
411        mCalendar.set(Calendar.YEAR, year);
412        mCalendar.set(Calendar.MONTH, month);
413        mCalendar.set(Calendar.DAY_OF_MONTH, day);
414        updatePickers();
415        updateDisplay(true);
416    }
417
418    private void updatePickers() {
419        Iterator<OnDateChangedListener> iterator = mListeners.iterator();
420        while (iterator.hasNext()) {
421            iterator.next().onDateChanged();
422        }
423    }
424
425
426    @Override
427    public CalendarDay getSelectedDay() {
428        return new CalendarDay(mCalendar);
429    }
430
431    @Override
432    public int getMinYear() {
433        return mMinYear;
434    }
435
436    @Override
437    public int getMaxYear() {
438        return mMaxYear;
439    }
440
441    @Override
442    public int getFirstDayOfWeek() {
443        return mWeekStart;
444    }
445
446    @Override
447    public void registerOnDateChangedListener(OnDateChangedListener listener) {
448        mListeners.add(listener);
449    }
450
451    @Override
452    public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
453        mListeners.remove(listener);
454    }
455
456    /**
457     * Try to vibrate. To prevent this becoming a single continuous vibration, nothing will
458     * happen if we have vibrated very recently.
459     */
460    @Override
461    public void tryVibrate() {
462        if (mVibrator != null) {
463            long now = SystemClock.uptimeMillis();
464            // We want to try to vibrate each individual tick discretely.
465            if (now - mLastVibrate >= 125) {
466                mVibrator.vibrate(5);
467                mLastVibrate = now;
468            }
469        }
470    }
471}
472