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 
17 package com.android.calendar.month;
18 
19 import android.app.Activity;
20 import android.app.FragmentManager;
21 import android.app.LoaderManager;
22 import android.content.ContentUris;
23 import android.content.CursorLoader;
24 import android.content.Loader;
25 import android.content.res.Resources;
26 import android.database.Cursor;
27 import android.graphics.drawable.StateListDrawable;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.Message;
32 import android.provider.CalendarContract.Attendees;
33 import android.provider.CalendarContract.Calendars;
34 import android.provider.CalendarContract.Instances;
35 import android.text.format.DateUtils;
36 import android.text.format.Time;
37 import android.util.Log;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.View.OnTouchListener;
42 import android.view.ViewConfiguration;
43 import android.view.ViewGroup;
44 import android.widget.AbsListView;
45 import android.widget.AbsListView.OnScrollListener;
46 
47 import com.android.calendar.CalendarController;
48 import com.android.calendar.CalendarController.EventInfo;
49 import com.android.calendar.CalendarController.EventType;
50 import com.android.calendar.CalendarController.ViewType;
51 import com.android.calendar.Event;
52 import com.android.calendar.R;
53 import com.android.calendar.Utils;
54 import com.android.calendar.event.CreateEventDialogFragment;
55 
56 import java.util.ArrayList;
57 import java.util.Calendar;
58 import java.util.HashMap;
59 import java.util.List;
60 
61 public 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      */
updateUri()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
updateLoadedDays()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 
updateWhere()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 
stopLoader()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
onAttach(Activity activity)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
onDetach()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
setUpAdapter()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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onActivityCreated(Bundle savedInstanceState)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 
MonthByWeekFragment()319     public MonthByWeekFragment() {
320         this(System.currentTimeMillis(), true);
321     }
322 
MonthByWeekFragment(long initialTime, boolean isMiniMonth)323     public MonthByWeekFragment(long initialTime, boolean isMiniMonth) {
324         super(initialTime);
325         mIsMiniMonth = isMiniMonth;
326     }
327 
328     @Override
setUpHeader()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
onCreateLoader(int id, Bundle args)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
doResumeUpdates()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
onLoadFinished(Loader<Cursor> loader, Cursor data)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
onLoaderReset(Loader<Cursor> loader)409     public void onLoaderReset(Loader<Cursor> loader) {
410     }
411 
412     @Override
eventsChanged()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
getSupportedEventTypes()421     public long getSupportedEventTypes() {
422         return EventType.GO_TO | EventType.EVENTS_CHANGED;
423     }
424 
425     @Override
handleEvent(EventInfo event)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
setMonthDisplayed(Time time, boolean updateHighlight)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
onScrollStateChanged(AbsListView view, int scrollState)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
onTouch(View v, MotionEvent event)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