MonthByWeekFragment.java revision 092caec951caa0fabcd51729678e3ddcd6ca2f03
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[] WHERE_CALENDARS_VISIBLE_ARGS = {"1"};
65    private static final String INSTANCES_SORT_ORDER = Instances.START_DAY + ","
66            + Instances.START_MINUTE + "," + Instances.TITLE;
67    protected static boolean mShowDetailsInMonth = false;
68
69    protected float mMinimumTwoMonthFlingVelocity;
70    protected boolean mIsMiniMonth;
71    protected boolean mHideDeclined;
72
73    protected int mFirstLoadedJulianDay;
74    protected int mLastLoadedJulianDay;
75
76    private static final int WEEKS_BUFFER = 1;
77    // How long to wait after scroll stops before starting the loader
78    // Using scroll duration because scroll state changes don't update
79    // correctly when a scroll is triggered programmatically.
80    private static final int LOADER_DELAY = 200;
81    // The minimum time between requeries of the data if the db is
82    // changing
83    private static final int LOADER_THROTTLE_DELAY = 500;
84
85    private CursorLoader mLoader;
86    private Uri mEventUri;
87    private GestureDetector mGestureDetector;
88    private Time mDesiredDay = new Time();
89
90    private volatile boolean mShouldLoad = true;
91    private boolean mUserScrolled = false;
92
93    private static float mScale = 0;
94    private static int SPACING_WEEK_NUMBER = 19;
95
96    private Runnable mTZUpdater = new Runnable() {
97        @Override
98        public void run() {
99            String tz = Utils.getTimeZone(mContext, mTZUpdater);
100            mSelectedDay.timezone = tz;
101            mSelectedDay.normalize(true);
102            mTempTime.timezone = tz;
103            mFirstDayOfMonth.timezone = tz;
104            mFirstDayOfMonth.normalize(true);
105            mFirstVisibleDay.timezone = tz;
106            mFirstVisibleDay.normalize(true);
107            if (mAdapter != null) {
108                mAdapter.refresh();
109            }
110        }
111    };
112
113
114    private Runnable mUpdateLoader = new Runnable() {
115        @Override
116        public void run() {
117            synchronized (this) {
118                if (!mShouldLoad || mLoader == null) {
119                    return;
120                }
121                // Stop any previous loads while we update the uri
122                stopLoader();
123
124                // Start the loader again
125                mEventUri = updateUri();
126                mLoader.setUri(mEventUri);
127                mLoader.startLoading();
128                mLoader.onContentChanged();
129                if (Log.isLoggable(TAG, Log.DEBUG)) {
130                    Log.d(TAG, "Started loader with uri: " + mEventUri);
131                }
132            }
133        }
134    };
135
136    /**
137     * Updates the uri used by the loader according to the current position of
138     * the listview.
139     *
140     * @return The new Uri to use
141     */
142    private Uri updateUri() {
143        SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0);
144        if (child != null) {
145            int julianDay = child.getFirstJulianDay();
146            mFirstLoadedJulianDay = julianDay;
147        }
148        // -1 to ensure we get all day events from any time zone
149        mTempTime.setJulianDay(mFirstLoadedJulianDay - 1);
150        long start = mTempTime.toMillis(true);
151        mLastLoadedJulianDay = mFirstLoadedJulianDay + (mNumWeeks + 2 * WEEKS_BUFFER) * 7;
152        // +1 to ensure we get all day events from any time zone
153        mTempTime.setJulianDay(mLastLoadedJulianDay + 1);
154        long end = mTempTime.toMillis(true);
155
156        // Create a new uri with the updated times
157        Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
158        ContentUris.appendId(builder, start);
159        ContentUris.appendId(builder, end);
160        return builder.build();
161    }
162
163    protected String updateWhere() {
164        // TODO fix selection/selection args after b/3206641 is fixed
165        String where = WHERE_CALENDARS_VISIBLE;
166        if (mHideDeclined) {
167            where += " AND " + Instances.SELF_ATTENDEE_STATUS + "!="
168                    + Attendees.ATTENDEE_STATUS_DECLINED;
169        }
170        return where;
171    }
172
173    private void stopLoader() {
174        synchronized (mUpdateLoader) {
175            mHandler.removeCallbacks(mUpdateLoader);
176            if (mLoader != null) {
177                mLoader.stopLoading();
178                if (Log.isLoggable(TAG, Log.DEBUG)) {
179                    Log.d(TAG, "Stopped loader from loading");
180                }
181            }
182        }
183    }
184
185    class MonthGestureListener extends SimpleOnGestureListener {
186        @Override
187        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
188                float velocityY) {
189            // TODO decide how to handle flings
190//            float absX = Math.abs(velocityX);
191//            float absY = Math.abs(velocityY);
192//            Log.d(TAG, "velX: " + velocityX + " velY: " + velocityY);
193//            if (absX > absY && absX > mMinimumFlingVelocity) {
194//                mTempTime.set(mFirstDayOfMonth);
195//                if(velocityX > 0) {
196//                    mTempTime.month++;
197//                } else {
198//                    mTempTime.month--;
199//                }
200//                mTempTime.normalize(true);
201//                goTo(mTempTime, true, false, true);
202//
203//            } else if (absY > absX && absY > mMinimumFlingVelocity) {
204//                mTempTime.set(mFirstDayOfMonth);
205//                int diff = 1;
206//                if (absY > mMinimumTwoMonthFlingVelocity) {
207//                    diff = 2;
208//                }
209//                if(velocityY < 0) {
210//                    mTempTime.month += diff;
211//                } else {
212//                    mTempTime.month -= diff;
213//                }
214//                mTempTime.normalize(true);
215//
216//                goTo(mTempTime, true, false, true);
217//            }
218            return false;
219        }
220    }
221
222    @Override
223    public void onAttach(Activity activity) {
224        super.onAttach(activity);
225        mTZUpdater.run();
226        if (mAdapter != null) {
227            mAdapter.setSelectedDay(mSelectedDay);
228        }
229
230        mGestureDetector = new GestureDetector(activity, new MonthGestureListener());
231        ViewConfiguration viewConfig = ViewConfiguration.get(activity);
232        mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity() / 2;
233
234        if (mScale == 0) {
235            Resources res = activity.getResources();
236            mScale = res.getDisplayMetrics().density;
237            mShowDetailsInMonth = res.getBoolean(R.bool.show_details_in_month);
238            if (mScale != 1) {
239                SPACING_WEEK_NUMBER *= mScale;
240            }
241        }
242    }
243
244    @Override
245    protected void setUpAdapter() {
246        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
247        mShowWeekNumber = Utils.getShowWeekNumber(mContext);
248
249        HashMap<String, Integer> weekParams = new HashMap<String, Integer>();
250        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks);
251        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0);
252        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek);
253        weekParams.put(MonthByWeekAdapter.WEEK_PARAMS_IS_MINI, mIsMiniMonth ? 1 : 0);
254        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY,
255                Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff));
256        weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_DAYS_PER_WEEK, mDaysPerWeek);
257        if (mAdapter == null) {
258            mAdapter = new MonthByWeekAdapter(getActivity(), weekParams);
259            mAdapter.registerDataSetObserver(mObserver);
260        } else {
261            mAdapter.updateParams(weekParams);
262        }
263        mAdapter.notifyDataSetChanged();
264    }
265
266    @Override
267    public View onCreateView(
268            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
269        View v;
270        if (mIsMiniMonth) {
271            v = inflater.inflate(R.layout.month_by_week, container, false);
272        } else {
273            v = inflater.inflate(R.layout.full_month_by_week, container, false);
274        }
275        mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names);
276        return v;
277    }
278
279    @Override
280    public void onActivityCreated(Bundle savedInstanceState) {
281        super.onActivityCreated(savedInstanceState);
282        mListView.setOnTouchListener(this);
283        getLoaderManager().initLoader(0, null, this);
284    }
285
286    public MonthByWeekFragment() {
287        this(System.currentTimeMillis(), true);
288    }
289
290    public MonthByWeekFragment(long initialTime, boolean isMiniMonth) {
291        super(initialTime);
292        mIsMiniMonth = isMiniMonth;
293    }
294
295    @Override
296    protected void setUpHeader() {
297        if (mIsMiniMonth) {
298            super.setUpHeader();
299            return;
300        }
301
302        mDayLabels = new String[7];
303        for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
304            mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(
305                    i, DateUtils.LENGTH_MEDIUM);
306        }
307    }
308
309    // TODO
310    @Override
311    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
312        if (mIsMiniMonth) {
313            return null;
314        }
315        synchronized (mUpdateLoader) {
316            mFirstLoadedJulianDay =
317                    Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff)
318                    - (mNumWeeks * 7 / 2);
319            mEventUri = updateUri();
320            String where = updateWhere();
321
322            mLoader = new CursorLoader(
323                    getActivity(), mEventUri, Event.EVENT_PROJECTION, where,
324                    null /* WHERE_CALENDARS_SELECTED_ARGS */, INSTANCES_SORT_ORDER);
325            mLoader.setUpdateThrottle(LOADER_THROTTLE_DELAY);
326        }
327        if (Log.isLoggable(TAG, Log.DEBUG)) {
328            Log.d(TAG, "Returning new loader with uri: " + mEventUri);
329        }
330        return mLoader;
331    }
332
333    @Override
334    public void doResumeUpdates() {
335        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
336        mShowWeekNumber = Utils.getShowWeekNumber(mContext);
337        boolean prevHideDeclined = mHideDeclined;
338        mHideDeclined = Utils.getHideDeclinedEvents(mContext);
339        if (prevHideDeclined != mHideDeclined && mLoader != null) {
340            mLoader.setSelection(updateWhere());
341        }
342        mDaysPerWeek = Utils.getDaysPerWeek(mContext);
343        updateHeader();
344        mAdapter.setSelectedDay(mSelectedDay);
345        mTZUpdater.run();
346        mTodayUpdater.run();
347        goTo(mSelectedDay.toMillis(true), false, true, false);
348    }
349
350    @Override
351    protected void updateHeader() {
352        super.updateHeader();
353        if (!mIsMiniMonth && mShowDetailsInMonth) {
354            mDayNamesHeader.findViewById(R.id.wk_label).setVisibility(View.VISIBLE);
355        }
356    }
357
358    @Override
359    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
360        synchronized (mUpdateLoader) {
361            if (Log.isLoggable(TAG, Log.DEBUG)) {
362                Log.d(TAG, "Found " + data.getCount() + " cursor entries for uri " + mEventUri);
363            }
364            CursorLoader cLoader = (CursorLoader) loader;
365            if (cLoader.getUri().compareTo(mEventUri) != 0) {
366                // We've started a new query since this loader ran so ignore the
367                // result
368                return;
369            }
370            ArrayList<Event> events = new ArrayList<Event>();
371            Event.buildEventsFromCursor(
372                    events, data, mContext, mFirstLoadedJulianDay, mLastLoadedJulianDay);
373            ((MonthByWeekAdapter) mAdapter).setEvents(mFirstLoadedJulianDay,
374                    mLastLoadedJulianDay - mFirstLoadedJulianDay + 1, events);
375        }
376    }
377
378    public void onLoaderReset(Loader<Cursor> loader) {
379    }
380
381    @Override
382    public void eventsChanged() {
383        // TODO remove this after b/3387924 is resolved
384        if (mLoader != null) {
385            mLoader.forceLoad();
386        }
387    }
388
389    @Override
390    public long getSupportedEventTypes() {
391        return EventType.GO_TO | EventType.EVENTS_CHANGED;
392    }
393
394    @Override
395    public void handleEvent(EventInfo event) {
396        if (event.eventType == EventType.GO_TO) {
397            boolean animate = true;
398            if (mDaysPerWeek * mNumWeeks * 2 < Math.abs(
399                    Time.getJulianDay(event.selectedTime.toMillis(true), event.selectedTime.gmtoff)
400                    - Time.getJulianDay(mFirstVisibleDay.toMillis(true), mFirstVisibleDay.gmtoff)
401                    - mDaysPerWeek * mNumWeeks / 2)) {
402                animate = false;
403            }
404            mDesiredDay.set(event.selectedTime);
405            mDesiredDay.normalize(true);
406            goTo(event.selectedTime.toMillis(true), animate, true, false);
407        } else if (event.eventType == EventType.EVENTS_CHANGED) {
408            eventsChanged();
409        }
410    }
411
412    @Override
413    protected void setMonthDisplayed(Time time) {
414        super.setMonthDisplayed(time);
415        if (!mIsMiniMonth) {
416            boolean useSelected = false;
417            if (time.year == mDesiredDay.year && time.month == mDesiredDay.month) {
418                mSelectedDay.set(mDesiredDay);
419                mAdapter.setSelectedDay(mDesiredDay);
420                useSelected = true;
421            } else {
422                mSelectedDay.set(time);
423                mAdapter.setSelectedDay(time);
424            }
425            CalendarController controller = CalendarController.getInstance(mContext);
426            if (mSelectedDay.minute >= 30) {
427                mSelectedDay.minute = 30;
428            } else {
429                mSelectedDay.minute = 0;
430            }
431            long newTime = mSelectedDay.normalize(true);
432            if (newTime != controller.getTime() && mUserScrolled) {
433                long offset = useSelected ? 0 : DateUtils.WEEK_IN_MILLIS * mNumWeeks / 3;
434                controller.setTime(newTime + offset);
435            }
436            controller.sendEvent(this, EventType.UPDATE_TITLE, time, null, null, -1,
437                    ViewType.CURRENT, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
438                            | DateUtils.FORMAT_SHOW_YEAR, null, null);
439        }
440    }
441
442    @Override
443    public void onScrollStateChanged(AbsListView view, int scrollState) {
444
445        synchronized (mUpdateLoader) {
446            if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
447                mShouldLoad = false;
448                stopLoader();
449                mDesiredDay.setToNow();
450            } else {
451                mHandler.removeCallbacks(mUpdateLoader);
452                mShouldLoad = true;
453                mHandler.postDelayed(mUpdateLoader, LOADER_DELAY);
454            }
455        }
456        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
457            mUserScrolled = true;
458        }
459
460        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
461    }
462
463    @Override
464    public boolean onTouch(View v, MotionEvent event) {
465        mDesiredDay.setToNow();
466        return mGestureDetector.onTouchEvent(event);
467        // TODO post a cleanup to push us back onto the grid if something went
468        // wrong in a scroll such as the user stopping the view but not
469        // scrolling
470    }
471
472}
473