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