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