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    private Runnable mTZUpdater = new Runnable() {
96        @Override
97        public void run() {
98            String tz = Utils.getTimeZone(mContext, mTZUpdater);
99            mSelectedDay.timezone = tz;
100            mSelectedDay.normalize(true);
101            mTempTime.timezone = tz;
102            mFirstDayOfMonth.timezone = tz;
103            mFirstDayOfMonth.normalize(true);
104            mFirstVisibleDay.timezone = tz;
105            mFirstVisibleDay.normalize(true);
106            if (mAdapter != null) {
107                mAdapter.refresh();
108            }
109        }
110    };
111
112
113    private Runnable mUpdateLoader = new Runnable() {
114        @Override
115        public void run() {
116            synchronized (this) {
117                if (!mShouldLoad || mLoader == null) {
118                    return;
119                }
120                // Stop any previous loads while we update the uri
121                stopLoader();
122
123                // Start the loader again
124                mEventUri = updateUri();
125
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 || !mShowDetailsInMonth) {
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        mLoader = (CursorLoader) 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(i,
305                    DateUtils.LENGTH_MEDIUM).toUpperCase();
306        }
307    }
308
309    // TODO
310    @Override
311    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
312        if (mIsMiniMonth) {
313            return null;
314        }
315        CursorLoader loader;
316        synchronized (mUpdateLoader) {
317            mFirstLoadedJulianDay =
318                    Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff)
319                    - (mNumWeeks * 7 / 2);
320            mEventUri = updateUri();
321            String where = updateWhere();
322
323            loader = new CursorLoader(
324                    getActivity(), mEventUri, Event.EVENT_PROJECTION, where,
325                    null /* WHERE_CALENDARS_SELECTED_ARGS */, INSTANCES_SORT_ORDER);
326            loader.setUpdateThrottle(LOADER_THROTTLE_DELAY);
327        }
328        if (Log.isLoggable(TAG, Log.DEBUG)) {
329            Log.d(TAG, "Returning new loader with uri: " + mEventUri);
330        }
331        return loader;
332    }
333
334    @Override
335    public void doResumeUpdates() {
336        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
337        mShowWeekNumber = Utils.getShowWeekNumber(mContext);
338        boolean prevHideDeclined = mHideDeclined;
339        mHideDeclined = Utils.getHideDeclinedEvents(mContext);
340        if (prevHideDeclined != mHideDeclined && mLoader != null) {
341            mLoader.setSelection(updateWhere());
342        }
343        mDaysPerWeek = Utils.getDaysPerWeek(mContext);
344        updateHeader();
345        mAdapter.setSelectedDay(mSelectedDay);
346        mTZUpdater.run();
347        mTodayUpdater.run();
348        goTo(mSelectedDay.toMillis(true), false, true, false);
349    }
350
351    @Override
352    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
353        synchronized (mUpdateLoader) {
354            if (Log.isLoggable(TAG, Log.DEBUG)) {
355                Log.d(TAG, "Found " + data.getCount() + " cursor entries for uri " + mEventUri);
356            }
357            CursorLoader cLoader = (CursorLoader) loader;
358            if (mEventUri == null) {
359                mEventUri = cLoader.getUri();
360            }
361            if (cLoader.getUri().compareTo(mEventUri) != 0) {
362                // We've started a new query since this loader ran so ignore the
363                // result
364                return;
365            }
366            ArrayList<Event> events = new ArrayList<Event>();
367            Event.buildEventsFromCursor(
368                    events, data, mContext, mFirstLoadedJulianDay, mLastLoadedJulianDay);
369            ((MonthByWeekAdapter) mAdapter).setEvents(mFirstLoadedJulianDay,
370                    mLastLoadedJulianDay - mFirstLoadedJulianDay + 1, events);
371        }
372    }
373
374    @Override
375    public void onLoaderReset(Loader<Cursor> loader) {
376    }
377
378    @Override
379    public void eventsChanged() {
380        // TODO remove this after b/3387924 is resolved
381        if (mLoader != null) {
382            mLoader.forceLoad();
383        }
384    }
385
386    @Override
387    public long getSupportedEventTypes() {
388        return EventType.GO_TO | EventType.EVENTS_CHANGED;
389    }
390
391    @Override
392    public void handleEvent(EventInfo event) {
393        if (event.eventType == EventType.GO_TO) {
394            boolean animate = true;
395            if (mDaysPerWeek * mNumWeeks * 2 < Math.abs(
396                    Time.getJulianDay(event.selectedTime.toMillis(true), event.selectedTime.gmtoff)
397                    - Time.getJulianDay(mFirstVisibleDay.toMillis(true), mFirstVisibleDay.gmtoff)
398                    - mDaysPerWeek * mNumWeeks / 2)) {
399                animate = false;
400            }
401            mDesiredDay.set(event.selectedTime);
402            mDesiredDay.normalize(true);
403            boolean animateToday = (event.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0;
404            boolean delayAnimation = goTo(event.selectedTime.toMillis(true), animate, true, false);
405            if (animateToday) {
406                // If we need to flash today start the animation after any
407                // movement from listView has ended.
408                mHandler.postDelayed(new Runnable() {
409                    @Override
410                    public void run() {
411                        ((MonthByWeekAdapter) mAdapter).animateToday();
412                        mAdapter.notifyDataSetChanged();
413                    }
414                }, delayAnimation ? GOTO_SCROLL_DURATION : 0);
415            }
416        } else if (event.eventType == EventType.EVENTS_CHANGED) {
417            eventsChanged();
418        }
419    }
420
421    @Override
422    protected void setMonthDisplayed(Time time, boolean updateHighlight) {
423        super.setMonthDisplayed(time, updateHighlight);
424        if (!mIsMiniMonth) {
425            boolean useSelected = false;
426            if (time.year == mDesiredDay.year && time.month == mDesiredDay.month) {
427                mSelectedDay.set(mDesiredDay);
428                mAdapter.setSelectedDay(mDesiredDay);
429                useSelected = true;
430            } else {
431                mSelectedDay.set(time);
432                mAdapter.setSelectedDay(time);
433            }
434            CalendarController controller = CalendarController.getInstance(mContext);
435            if (mSelectedDay.minute >= 30) {
436                mSelectedDay.minute = 30;
437            } else {
438                mSelectedDay.minute = 0;
439            }
440            long newTime = mSelectedDay.normalize(true);
441            if (newTime != controller.getTime() && mUserScrolled) {
442                long offset = useSelected ? 0 : DateUtils.WEEK_IN_MILLIS * mNumWeeks / 3;
443                controller.setTime(newTime + offset);
444            }
445            controller.sendEvent(this, EventType.UPDATE_TITLE, time, time, time, -1,
446                    ViewType.CURRENT, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
447                            | DateUtils.FORMAT_SHOW_YEAR, null, null);
448        }
449    }
450
451    @Override
452    public void onScrollStateChanged(AbsListView view, int scrollState) {
453
454        synchronized (mUpdateLoader) {
455            if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
456                mShouldLoad = false;
457                stopLoader();
458                mDesiredDay.setToNow();
459            } else {
460                mHandler.removeCallbacks(mUpdateLoader);
461                mShouldLoad = true;
462                mHandler.postDelayed(mUpdateLoader, LOADER_DELAY);
463            }
464        }
465        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
466            mUserScrolled = true;
467        }
468
469        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
470    }
471
472    @Override
473    public boolean onTouch(View v, MotionEvent event) {
474        mDesiredDay.setToNow();
475        return mGestureDetector.onTouchEvent(event);
476        // TODO post a cleanup to push us back onto the grid if something went
477        // wrong in a scroll such as the user stopping the view but not
478        // scrolling
479    }
480
481}
482