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