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 com.android.calendar.CalendarController;
     20 import com.android.calendar.CalendarController.EventInfo;
     21 import com.android.calendar.CalendarController.EventType;
     22 import com.android.calendar.CalendarController.ViewType;
     23 import com.android.calendar.Event;
     24 import com.android.calendar.R;
     25 import com.android.calendar.Utils;
     26 
     27 import android.app.Activity;
     28 import android.app.LoaderManager;
     29 import android.content.ContentUris;
     30 import android.content.CursorLoader;
     31 import android.content.Loader;
     32 import android.content.res.Resources;
     33 import android.database.Cursor;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.provider.CalendarContract.Attendees;
     37 import android.provider.CalendarContract.Calendars;
     38 import android.provider.CalendarContract.Instances;
     39 import android.text.format.DateUtils;
     40 import android.text.format.Time;
     41 import android.util.Log;
     42 import android.view.GestureDetector;
     43 import android.view.GestureDetector.SimpleOnGestureListener;
     44 import android.view.LayoutInflater;
     45 import android.view.MotionEvent;
     46 import android.view.View;
     47 import android.view.View.OnTouchListener;
     48 import android.view.ViewConfiguration;
     49 import android.view.ViewGroup;
     50 import android.widget.AbsListView;
     51 import android.widget.AbsListView.OnScrollListener;
     52 
     53 import java.util.ArrayList;
     54 import java.util.Calendar;
     55 import java.util.HashMap;
     56 
     57 public 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