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.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      */
    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
    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 
    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 
    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
    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
    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
    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
    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
    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 
    319     public MonthByWeekFragment() {
    320         this(System.currentTimeMillis(), true);
    321     }
    322 
    323     public MonthByWeekFragment(long initialTime, boolean isMiniMonth) {
    324         super(initialTime);
    325         mIsMiniMonth = isMiniMonth;
    326     }
    327 
    328     @Override
    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
    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
    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
    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
    409     public void onLoaderReset(Loader<Cursor> loader) {
    410     }
    411 
    412     @Override
    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
    421     public long getSupportedEventTypes() {
    422         return EventType.GO_TO | EventType.EVENTS_CHANGED;
    423     }
    424 
    425     @Override
    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
    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
    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
    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