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