1/*
2 * Copyright (C) 2010 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.calendar.month;
18
19import android.app.Activity;
20import android.app.FragmentManager;
21import android.app.LoaderManager;
22import android.content.ContentUris;
23import android.content.CursorLoader;
24import android.content.Loader;
25import android.content.res.Resources;
26import android.database.Cursor;
27import android.graphics.drawable.StateListDrawable;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Message;
32import android.provider.CalendarContract.Attendees;
33import android.provider.CalendarContract.Calendars;
34import android.provider.CalendarContract.Instances;
35import android.text.format.DateUtils;
36import android.text.format.Time;
37import android.util.Log;
38import android.view.LayoutInflater;
39import android.view.MotionEvent;
40import android.view.View;
41import android.view.View.OnTouchListener;
42import android.view.ViewConfiguration;
43import android.view.ViewGroup;
44import android.widget.AbsListView;
45import android.widget.AbsListView.OnScrollListener;
46
47import com.android.calendar.CalendarController;
48import com.android.calendar.CalendarController.EventInfo;
49import com.android.calendar.CalendarController.EventType;
50import com.android.calendar.CalendarController.ViewType;
51import com.android.calendar.Event;
52import com.android.calendar.R;
53import com.android.calendar.Utils;
54import com.android.calendar.event.CreateEventDialogFragment;
55
56import java.util.ArrayList;
57import java.util.Calendar;
58import java.util.HashMap;
59import java.util.List;
60
61public class MonthByWeekFragment extends SimpleDayPickerFragment implements
62        CalendarController.EventHandler, LoaderManager.LoaderCallbacks<Cursor>, OnScrollListener,
63        OnTouchListener {
64    private static final String TAG = "MonthFragment";
65    private static final String TAG_EVENT_DIALOG = "event_dialog";
66
67    private CreateEventDialogFragment mEventDialog;
68
69    // Selection and selection args for adding event queries
70    private static final String WHERE_CALENDARS_VISIBLE = Calendars.VISIBLE + "=1";
71    private static final String INSTANCES_SORT_ORDER = Instances.START_DAY + ","
72            + Instances.START_MINUTE + "," + Instances.TITLE;
73    protected static boolean mShowDetailsInMonth = false;
74
75    protected float mMinimumTwoMonthFlingVelocity;
76    protected boolean mIsMiniMonth;
77    protected boolean mHideDeclined;
78
79    protected int mFirstLoadedJulianDay;
80    protected int mLastLoadedJulianDay;
81
82    private static final int WEEKS_BUFFER = 1;
83    // How long to wait after scroll stops before starting the loader
84    // Using scroll duration because scroll state changes don't update
85    // correctly when a scroll is triggered programmatically.
86    private static final int LOADER_DELAY = 200;
87    // The minimum time between requeries of the data if the db is
88    // changing
89    private static final int LOADER_THROTTLE_DELAY = 500;
90
91    private CursorLoader mLoader;
92    private Uri mEventUri;
93    private final Time mDesiredDay = new Time();
94
95    private volatile boolean mShouldLoad = true;
96    private boolean mUserScrolled = false;
97
98    private int mEventsLoadingDelay;
99    private boolean mShowCalendarControls;
100    private boolean mIsDetached;
101
102    private Handler mEventDialogHandler = new Handler() {
103
104        @Override
105        public void handleMessage(Message msg) {
106            final FragmentManager manager = getFragmentManager();
107            if (manager != null) {
108                Time day = (Time) msg.obj;
109                mEventDialog = new CreateEventDialogFragment(day);
110                mEventDialog.show(manager, TAG_EVENT_DIALOG);
111            }
112        }
113    };
114
115
116    private final Runnable mTZUpdater = new Runnable() {
117        @Override
118        public void run() {
119            String tz = Utils.getTimeZone(mContext, mTZUpdater);
120            mSelectedDay.timezone = tz;
121            mSelectedDay.normalize(true);
122            mTempTime.timezone = tz;
123            mFirstDayOfMonth.timezone = tz;
124            mFirstDayOfMonth.normalize(true);
125            mFirstVisibleDay.timezone = tz;
126            mFirstVisibleDay.normalize(true);
127            if (mAdapter != null) {
128                mAdapter.refresh();
129            }
130        }
131    };
132
133
134    private final Runnable mUpdateLoader = new Runnable() {
135        @Override
136        public void run() {
137            synchronized (this) {
138                if (!mShouldLoad || mLoader == null) {
139                    return;
140                }
141                // Stop any previous loads while we update the uri
142                stopLoader();
143
144                // Start the loader again
145                mEventUri = updateUri();
146
147                mLoader.setUri(mEventUri);
148                mLoader.startLoading();
149                mLoader.onContentChanged();
150                if (Log.isLoggable(TAG, Log.DEBUG)) {
151                    Log.d(TAG, "Started loader with uri: " + mEventUri);
152                }
153            }
154        }
155    };
156    // Used to load the events when a delay is needed
157    Runnable mLoadingRunnable = new Runnable() {
158        @Override
159        public void run() {
160            if (!mIsDetached) {
161                mLoader = (CursorLoader) getLoaderManager().initLoader(0, null,
162                        MonthByWeekFragment.this);
163            }
164        }
165    };
166
167
168    /**
169     * Updates the uri used by the loader according to the current position of
170     * the listview.
171     *
172     * @return The new Uri to use
173     */
174    private Uri updateUri() {
175        SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0);
176        if (child != null) {
177            int julianDay = child.getFirstJulianDay();
178            mFirstLoadedJulianDay = julianDay;
179        }
180        // -1 to ensure we get all day events from any time zone
181        mTempTime.setJulianDay(mFirstLoadedJulianDay - 1);
182        long start = mTempTime.toMillis(true);
183        mLastLoadedJulianDay = mFirstLoadedJulianDay + (mNumWeeks + 2 * WEEKS_BUFFER) * 7;
184        // +1 to ensure we get all day events from any time zone
185        mTempTime.setJulianDay(mLastLoadedJulianDay + 1);
186        long end = mTempTime.toMillis(true);
187
188        // Create a new uri with the updated times
189        Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
190        ContentUris.appendId(builder, start);
191        ContentUris.appendId(builder, end);
192        return builder.build();
193    }
194
195    // Extract range of julian days from URI
196    private void updateLoadedDays() {
197        List<String> pathSegments = mEventUri.getPathSegments();
198        int size = pathSegments.size();
199        if (size <= 2) {
200            return;
201        }
202        long first = Long.parseLong(pathSegments.get(size - 2));
203        long last = Long.parseLong(pathSegments.get(size - 1));
204        mTempTime.set(first);
205        mFirstLoadedJulianDay = Time.getJulianDay(first, mTempTime.gmtoff);
206        mTempTime.set(last);
207        mLastLoadedJulianDay = Time.getJulianDay(last, mTempTime.gmtoff);
208    }
209
210    protected String updateWhere() {
211        // TODO fix selection/selection args after b/3206641 is fixed
212        String where = WHERE_CALENDARS_VISIBLE;
213        if (mHideDeclined || !mShowDetailsInMonth) {
214            where += " AND " + Instances.SELF_ATTENDEE_STATUS + "!="
215                    + Attendees.ATTENDEE_STATUS_DECLINED;
216        }
217        return where;
218    }
219
220    private void stopLoader() {
221        synchronized (mUpdateLoader) {
222            mHandler.removeCallbacks(mUpdateLoader);
223            if (mLoader != null) {
224                mLoader.stopLoading();
225                if (Log.isLoggable(TAG, Log.DEBUG)) {
226                    Log.d(TAG, "Stopped loader from loading");
227                }
228            }
229        }
230    }
231
232    @Override
233    public void onAttach(Activity activity) {
234        super.onAttach(activity);
235        mTZUpdater.run();
236        if (mAdapter != null) {
237            mAdapter.setSelectedDay(mSelectedDay);
238        }
239        mIsDetached = false;
240
241        ViewConfiguration viewConfig = ViewConfiguration.get(activity);
242        mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity() / 2;
243        Resources res = activity.getResources();
244        mShowCalendarControls = Utils.getConfigBool(activity, R.bool.show_calendar_controls);
245        // Synchronized the loading time of the month's events with the animation of the
246        // calendar controls.
247        if (mShowCalendarControls) {
248            mEventsLoadingDelay = res.getInteger(R.integer.calendar_controls_animation_time);
249        }
250        mShowDetailsInMonth = res.getBoolean(R.bool.show_details_in_month);
251    }
252
253    @Override
254    public void onDetach() {
255        mIsDetached = true;
256        super.onDetach();
257        if (mShowCalendarControls) {
258            if (mListView != null) {
259                mListView.removeCallbacks(mLoadingRunnable);
260            }
261        }
262    }
263
264    @Override
265    protected void setUpAdapter() {
266        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
267        mShowWeekNumber = Utils.getShowWeekNumber(mContext);
268
269        HashMap<String, Integer> weekParams = new HashMap<String, Integer>();
270        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks);
271        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0);
272        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek);
273        weekParams.put(MonthByWeekAdapter.WEEK_PARAMS_IS_MINI, mIsMiniMonth ? 1 : 0);
274        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY,
275                Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff));
276        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_DAYS_PER_WEEK, mDaysPerWeek);
277        if (mAdapter == null) {
278            mAdapter = new MonthByWeekAdapter(getActivity(), weekParams, mEventDialogHandler);
279            mAdapter.registerDataSetObserver(mObserver);
280        } else {
281            mAdapter.updateParams(weekParams);
282        }
283        mAdapter.notifyDataSetChanged();
284    }
285
286    @Override
287    public View onCreateView(
288            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
289        View v;
290        if (mIsMiniMonth) {
291            v = inflater.inflate(R.layout.month_by_week, container, false);
292        } else {
293            v = inflater.inflate(R.layout.full_month_by_week, container, false);
294        }
295        mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names);
296        return v;
297    }
298
299    @Override
300    public void onActivityCreated(Bundle savedInstanceState) {
301        super.onActivityCreated(savedInstanceState);
302        mListView.setSelector(new StateListDrawable());
303        mListView.setOnTouchListener(this);
304
305        if (!mIsMiniMonth) {
306            mListView.setBackgroundColor(getResources().getColor(R.color.month_bgcolor));
307        }
308
309        // To get a smoother transition when showing this fragment, delay loading of events until
310        // the fragment is expended fully and the calendar controls are gone.
311        if (mShowCalendarControls) {
312            mListView.postDelayed(mLoadingRunnable, mEventsLoadingDelay);
313        } else {
314            mLoader = (CursorLoader) getLoaderManager().initLoader(0, null, this);
315        }
316        mAdapter.setListView(mListView);
317    }
318
319    public MonthByWeekFragment() {
320        this(System.currentTimeMillis(), true);
321    }
322
323    public MonthByWeekFragment(long initialTime, boolean isMiniMonth) {
324        super(initialTime);
325        mIsMiniMonth = isMiniMonth;
326    }
327
328    @Override
329    protected void setUpHeader() {
330        if (mIsMiniMonth) {
331            super.setUpHeader();
332            return;
333        }
334
335        mDayLabels = new String[7];
336        for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
337            mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i,
338                    DateUtils.LENGTH_MEDIUM).toUpperCase();
339        }
340    }
341
342    // TODO
343    @Override
344    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
345        if (mIsMiniMonth) {
346            return null;
347        }
348        CursorLoader loader;
349        synchronized (mUpdateLoader) {
350            mFirstLoadedJulianDay =
351                    Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff)
352                    - (mNumWeeks * 7 / 2);
353            mEventUri = updateUri();
354            String where = updateWhere();
355
356            loader = new CursorLoader(
357                    getActivity(), mEventUri, Event.EVENT_PROJECTION, where,
358                    null /* WHERE_CALENDARS_SELECTED_ARGS */, INSTANCES_SORT_ORDER);
359            loader.setUpdateThrottle(LOADER_THROTTLE_DELAY);
360        }
361        if (Log.isLoggable(TAG, Log.DEBUG)) {
362            Log.d(TAG, "Returning new loader with uri: " + mEventUri);
363        }
364        return loader;
365    }
366
367    @Override
368    public void doResumeUpdates() {
369        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
370        mShowWeekNumber = Utils.getShowWeekNumber(mContext);
371        boolean prevHideDeclined = mHideDeclined;
372        mHideDeclined = Utils.getHideDeclinedEvents(mContext);
373        if (prevHideDeclined != mHideDeclined && mLoader != null) {
374            mLoader.setSelection(updateWhere());
375        }
376        mDaysPerWeek = Utils.getDaysPerWeek(mContext);
377        updateHeader();
378        mAdapter.setSelectedDay(mSelectedDay);
379        mTZUpdater.run();
380        mTodayUpdater.run();
381        goTo(mSelectedDay.toMillis(true), false, true, false);
382    }
383
384    @Override
385    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
386        synchronized (mUpdateLoader) {
387            if (Log.isLoggable(TAG, Log.DEBUG)) {
388                Log.d(TAG, "Found " + data.getCount() + " cursor entries for uri " + mEventUri);
389            }
390            CursorLoader cLoader = (CursorLoader) loader;
391            if (mEventUri == null) {
392                mEventUri = cLoader.getUri();
393                updateLoadedDays();
394            }
395            if (cLoader.getUri().compareTo(mEventUri) != 0) {
396                // We've started a new query since this loader ran so ignore the
397                // result
398                return;
399            }
400            ArrayList<Event> events = new ArrayList<Event>();
401            Event.buildEventsFromCursor(
402                    events, data, mContext, mFirstLoadedJulianDay, mLastLoadedJulianDay);
403            ((MonthByWeekAdapter) mAdapter).setEvents(mFirstLoadedJulianDay,
404                    mLastLoadedJulianDay - mFirstLoadedJulianDay + 1, events);
405        }
406    }
407
408    @Override
409    public void onLoaderReset(Loader<Cursor> loader) {
410    }
411
412    @Override
413    public void eventsChanged() {
414        // TODO remove this after b/3387924 is resolved
415        if (mLoader != null) {
416            mLoader.forceLoad();
417        }
418    }
419
420    @Override
421    public long getSupportedEventTypes() {
422        return EventType.GO_TO | EventType.EVENTS_CHANGED;
423    }
424
425    @Override
426    public void handleEvent(EventInfo event) {
427        if (event.eventType == EventType.GO_TO) {
428            boolean animate = true;
429            if (mDaysPerWeek * mNumWeeks * 2 < Math.abs(
430                    Time.getJulianDay(event.selectedTime.toMillis(true), event.selectedTime.gmtoff)
431                    - Time.getJulianDay(mFirstVisibleDay.toMillis(true), mFirstVisibleDay.gmtoff)
432                    - mDaysPerWeek * mNumWeeks / 2)) {
433                animate = false;
434            }
435            mDesiredDay.set(event.selectedTime);
436            mDesiredDay.normalize(true);
437            boolean animateToday = (event.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0;
438            boolean delayAnimation = goTo(event.selectedTime.toMillis(true), animate, true, false);
439            if (animateToday) {
440                // If we need to flash today start the animation after any
441                // movement from listView has ended.
442                mHandler.postDelayed(new Runnable() {
443                    @Override
444                    public void run() {
445                        ((MonthByWeekAdapter) mAdapter).animateToday();
446                        mAdapter.notifyDataSetChanged();
447                    }
448                }, delayAnimation ? GOTO_SCROLL_DURATION : 0);
449            }
450        } else if (event.eventType == EventType.EVENTS_CHANGED) {
451            eventsChanged();
452        }
453    }
454
455    @Override
456    protected void setMonthDisplayed(Time time, boolean updateHighlight) {
457        super.setMonthDisplayed(time, updateHighlight);
458        if (!mIsMiniMonth) {
459            boolean useSelected = false;
460            if (time.year == mDesiredDay.year && time.month == mDesiredDay.month) {
461                mSelectedDay.set(mDesiredDay);
462                mAdapter.setSelectedDay(mDesiredDay);
463                useSelected = true;
464            } else {
465                mSelectedDay.set(time);
466                mAdapter.setSelectedDay(time);
467            }
468            CalendarController controller = CalendarController.getInstance(mContext);
469            if (mSelectedDay.minute >= 30) {
470                mSelectedDay.minute = 30;
471            } else {
472                mSelectedDay.minute = 0;
473            }
474            long newTime = mSelectedDay.normalize(true);
475            if (newTime != controller.getTime() && mUserScrolled) {
476                long offset = useSelected ? 0 : DateUtils.WEEK_IN_MILLIS * mNumWeeks / 3;
477                controller.setTime(newTime + offset);
478            }
479            controller.sendEvent(this, EventType.UPDATE_TITLE, time, time, time, -1,
480                    ViewType.CURRENT, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
481                            | DateUtils.FORMAT_SHOW_YEAR, null, null);
482        }
483    }
484
485    @Override
486    public void onScrollStateChanged(AbsListView view, int scrollState) {
487
488        synchronized (mUpdateLoader) {
489            if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
490                mShouldLoad = false;
491                stopLoader();
492                mDesiredDay.setToNow();
493            } else {
494                mHandler.removeCallbacks(mUpdateLoader);
495                mShouldLoad = true;
496                mHandler.postDelayed(mUpdateLoader, LOADER_DELAY);
497            }
498        }
499        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
500            mUserScrolled = true;
501        }
502
503        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
504    }
505
506    @Override
507    public boolean onTouch(View v, MotionEvent event) {
508        mDesiredDay.setToNow();
509        return false;
510        // TODO post a cleanup to push us back onto the grid if something went
511        // wrong in a scroll such as the user stopping the view but not
512        // scrolling
513    }
514}
515