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.content.Context;
     20 import android.content.res.Configuration;
     21 import android.text.format.Time;
     22 import android.util.Log;
     23 import android.view.GestureDetector;
     24 import android.view.MotionEvent;
     25 import android.view.View;
     26 import android.view.ViewConfiguration;
     27 import android.view.ViewGroup;
     28 import android.widget.AbsListView.LayoutParams;
     29 
     30 import com.android.calendar.CalendarController;
     31 import com.android.calendar.CalendarController.EventType;
     32 import com.android.calendar.CalendarController.ViewType;
     33 import com.android.calendar.Event;
     34 import com.android.calendar.R;
     35 import com.android.calendar.Utils;
     36 
     37 import java.util.ArrayList;
     38 import java.util.HashMap;
     39 
     40 public class MonthByWeekAdapter extends SimpleWeeksAdapter {
     41     private static final String TAG = "MonthByWeek";
     42 
     43     public static final String WEEK_PARAMS_IS_MINI = "mini_month";
     44     protected static int DEFAULT_QUERY_DAYS = 7 * 8; // 8 weeks
     45     private static final long ANIMATE_TODAY_TIMEOUT = 1000;
     46 
     47     protected CalendarController mController;
     48     protected String mHomeTimeZone;
     49     protected Time mTempTime;
     50     protected Time mToday;
     51     protected int mFirstJulianDay;
     52     protected int mQueryDays;
     53     protected boolean mIsMiniMonth = true;
     54     protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE;
     55     private final boolean mShowAgendaWithMonth;
     56 
     57     protected ArrayList<ArrayList<Event>> mEventDayList = new ArrayList<ArrayList<Event>>();
     58     protected ArrayList<Event> mEvents = null;
     59 
     60     private boolean mAnimateToday = false;
     61     private long mAnimateTime = 0;
     62 
     63     MonthWeekEventsView mClickedView;
     64     MonthWeekEventsView mSingleTapUpView;
     65 
     66     float mClickedXLocation;                // Used to find which day was clicked
     67     long mClickTime;                        // Used to calculate minimum click animation time
     68     // Used to insure minimal time for seeing the click animation before switching views
     69     private static final int mOnTapDelay = 100;
     70     // Minimal time for a down touch action before stating the click animation, this insures that
     71     // there is no click animation on flings
     72     private static int mOnDownDelay;
     73     private static int mTotalClickDelay;
     74     // Minimal distance to move the finger in order to cancel the click animation
     75     private static float mMovedPixelToCancel;
     76 
     77     public MonthByWeekAdapter(Context context, HashMap<String, Integer> params) {
     78         super(context, params);
     79         if (params.containsKey(WEEK_PARAMS_IS_MINI)) {
     80             mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0;
     81         }
     82         mShowAgendaWithMonth = Utils.getConfigBool(context, R.bool.show_agenda_with_month);
     83         ViewConfiguration vc = ViewConfiguration.get(context);
     84         mOnDownDelay = ViewConfiguration.getTapTimeout();
     85         mMovedPixelToCancel = vc.getScaledTouchSlop();
     86         mTotalClickDelay = mOnDownDelay + mOnTapDelay;
     87 
     88     }
     89 
     90     public void animateToday() {
     91         mAnimateToday = true;
     92         mAnimateTime = System.currentTimeMillis();
     93     }
     94 
     95     @Override
     96     protected void init() {
     97         super.init();
     98         mController = CalendarController.getInstance(mContext);
     99         mHomeTimeZone = Utils.getTimeZone(mContext, null);
    100         mSelectedDay.switchTimezone(mHomeTimeZone);
    101         mToday = new Time(mHomeTimeZone);
    102         mToday.setToNow();
    103         mTempTime = new Time(mHomeTimeZone);
    104     }
    105 
    106     private void updateTimeZones() {
    107         mSelectedDay.timezone = mHomeTimeZone;
    108         mSelectedDay.normalize(true);
    109         mToday.timezone = mHomeTimeZone;
    110         mToday.setToNow();
    111         mTempTime.switchTimezone(mHomeTimeZone);
    112     }
    113 
    114     @Override
    115     public void setSelectedDay(Time selectedTime) {
    116         mSelectedDay.set(selectedTime);
    117         long millis = mSelectedDay.normalize(true);
    118         mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay(
    119                 Time.getJulianDay(millis, mSelectedDay.gmtoff), mFirstDayOfWeek);
    120         notifyDataSetChanged();
    121     }
    122 
    123     public void setEvents(int firstJulianDay, int numDays, ArrayList<Event> events) {
    124         if (mIsMiniMonth) {
    125             if (Log.isLoggable(TAG, Log.ERROR)) {
    126                 Log.e(TAG, "Attempted to set events for mini view. Events only supported in full"
    127                         + " view.");
    128             }
    129             return;
    130         }
    131         mEvents = events;
    132         mFirstJulianDay = firstJulianDay;
    133         mQueryDays = numDays;
    134         // Create a new list, this is necessary since the weeks are referencing
    135         // pieces of the old list
    136         ArrayList<ArrayList<Event>> eventDayList = new ArrayList<ArrayList<Event>>();
    137         for (int i = 0; i < numDays; i++) {
    138             eventDayList.add(new ArrayList<Event>());
    139         }
    140 
    141         if (events == null || events.size() == 0) {
    142             if(Log.isLoggable(TAG, Log.DEBUG)) {
    143                 Log.d(TAG, "No events. Returning early--go schedule something fun.");
    144             }
    145             mEventDayList = eventDayList;
    146             refresh();
    147             return;
    148         }
    149 
    150         // Compute the new set of days with events
    151         for (Event event : events) {
    152             int startDay = event.startDay - mFirstJulianDay;
    153             int endDay = event.endDay - mFirstJulianDay + 1;
    154             if (startDay < numDays || endDay >= 0) {
    155                 if (startDay < 0) {
    156                     startDay = 0;
    157                 }
    158                 if (startDay > numDays) {
    159                     continue;
    160                 }
    161                 if (endDay < 0) {
    162                     continue;
    163                 }
    164                 if (endDay > numDays) {
    165                     endDay = numDays;
    166                 }
    167                 for (int j = startDay; j < endDay; j++) {
    168                     eventDayList.get(j).add(event);
    169                 }
    170             }
    171         }
    172         if(Log.isLoggable(TAG, Log.DEBUG)) {
    173             Log.d(TAG, "Processed " + events.size() + " events.");
    174         }
    175         mEventDayList = eventDayList;
    176         refresh();
    177     }
    178 
    179     @SuppressWarnings("unchecked")
    180     @Override
    181     public View getView(int position, View convertView, ViewGroup parent) {
    182         if (mIsMiniMonth) {
    183             return super.getView(position, convertView, parent);
    184         }
    185         MonthWeekEventsView v;
    186         LayoutParams params = new LayoutParams(
    187                 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    188         HashMap<String, Integer> drawingParams = null;
    189         boolean isAnimatingToday = false;
    190         if (convertView != null) {
    191             v = (MonthWeekEventsView) convertView;
    192             // Checking updateToday uses the current params instead of the new
    193             // params, so this is assuming the view is relatively stable
    194             if (mAnimateToday && v.updateToday(mSelectedDay.timezone)) {
    195                 long currentTime = System.currentTimeMillis();
    196                 // If it's been too long since we tried to start the animation
    197                 // don't show it. This can happen if the user stops a scroll
    198                 // before reaching today.
    199                 if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) {
    200                     mAnimateToday = false;
    201                     mAnimateTime = 0;
    202                 } else {
    203                     isAnimatingToday = true;
    204                     // There is a bug that causes invalidates to not work some
    205                     // of the time unless we recreate the view.
    206                     v = new MonthWeekEventsView(mContext);
    207                 }
    208             } else {
    209                 drawingParams = (HashMap<String, Integer>) v.getTag();
    210             }
    211         } else {
    212             v = new MonthWeekEventsView(mContext);
    213         }
    214         if (drawingParams == null) {
    215             drawingParams = new HashMap<String, Integer>();
    216         }
    217         drawingParams.clear();
    218 
    219         v.setLayoutParams(params);
    220         v.setClickable(true);
    221         v.setOnTouchListener(this);
    222 
    223         int selectedDay = -1;
    224         if (mSelectedWeek == position) {
    225             selectedDay = mSelectedDay.weekDay;
    226         }
    227 
    228         drawingParams.put(SimpleWeekView.VIEW_PARAMS_HEIGHT,
    229                 (parent.getHeight() + parent.getTop()) / mNumWeeks);
    230         drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay);
    231         drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, mShowWeekNumber ? 1 : 0);
    232         drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek);
    233         drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek);
    234         drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position);
    235         drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth);
    236         drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation);
    237 
    238         if (isAnimatingToday) {
    239             drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1);
    240             mAnimateToday = false;
    241         }
    242 
    243         v.setWeekParams(drawingParams, mSelectedDay.timezone);
    244         sendEventsToView(v);
    245         return v;
    246     }
    247 
    248     private void sendEventsToView(MonthWeekEventsView v) {
    249         if (mEventDayList.size() == 0) {
    250             if (Log.isLoggable(TAG, Log.DEBUG)) {
    251                 Log.d(TAG, "No events loaded, did not pass any events to view.");
    252             }
    253             v.setEvents(null, null);
    254             return;
    255         }
    256         int viewJulianDay = v.getFirstJulianDay();
    257         int start = viewJulianDay - mFirstJulianDay;
    258         int end = start + v.mNumDays;
    259         if (start < 0 || end > mEventDayList.size()) {
    260             if (Log.isLoggable(TAG, Log.DEBUG)) {
    261                 Log.d(TAG, "Week is outside range of loaded events. viewStart: " + viewJulianDay
    262                         + " eventsStart: " + mFirstJulianDay);
    263             }
    264             v.setEvents(null, null);
    265             return;
    266         }
    267         v.setEvents(mEventDayList.subList(start, end), mEvents);
    268     }
    269 
    270     @Override
    271     protected void refresh() {
    272         mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
    273         mShowWeekNumber = Utils.getShowWeekNumber(mContext);
    274         mHomeTimeZone = Utils.getTimeZone(mContext, null);
    275         mOrientation = mContext.getResources().getConfiguration().orientation;
    276         updateTimeZones();
    277         notifyDataSetChanged();
    278     }
    279 
    280     @Override
    281     protected void onDayTapped(Time day) {
    282         day.timezone = mHomeTimeZone;
    283         Time currTime = new Time(mHomeTimeZone);
    284         currTime.set(mController.getTime());
    285         day.hour = currTime.hour;
    286         day.minute = currTime.minute;
    287         day.allDay = false;
    288         day.normalize(true);
    289          if (mShowAgendaWithMonth && !mIsMiniMonth) {
    290             // If agenda view is visible with month view , refresh the views
    291             // with the selected day's info
    292             mController.sendEvent(mContext, EventType.GO_TO, day, day, -1,
    293                     ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null);
    294         } else {
    295             // Else , switch to the detailed view
    296             mController.sendEvent(mContext, EventType.GO_TO, day, day, -1,
    297                     mIsMiniMonth ? ViewType.CURRENT : ViewType.DETAIL,
    298                     CalendarController.EXTRA_GOTO_DATE
    299                             | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null);
    300         }
    301     }
    302 
    303 
    304     @Override
    305     public boolean onTouch(View v, MotionEvent event) {
    306         if (!(v instanceof MonthWeekEventsView)) {
    307             return super.onTouch(v, event);
    308         }
    309 
    310         int action = event.getAction();
    311 
    312         // Event was tapped - switch to the detailed view making sure the click animation
    313         // is done first.
    314         if (mGestureDetector.onTouchEvent(event)) {
    315             mSingleTapUpView = (MonthWeekEventsView) v;
    316             long delay = System.currentTimeMillis() - mClickTime;
    317             // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms
    318             mListView.postDelayed(mDoSingleTapUp,
    319                     delay > mTotalClickDelay ? 0 : mTotalClickDelay - delay);
    320             return true;
    321         } else {
    322             // Animate a click - on down: show the selected day in the "clicked" color.
    323             // On Up/scroll/move/cancel: hide the "clicked" color.
    324             switch (action) {
    325                 case MotionEvent.ACTION_DOWN:
    326                     mClickedView = (MonthWeekEventsView)v;
    327                     mClickedXLocation = event.getX();
    328                     mClickTime = System.currentTimeMillis();
    329                     mListView.postDelayed(mDoClick, mOnDownDelay);
    330                     break;
    331                 case MotionEvent.ACTION_UP:
    332                 case MotionEvent.ACTION_SCROLL:
    333                 case MotionEvent.ACTION_CANCEL:
    334                     clearClickedView((MonthWeekEventsView)v);
    335                     break;
    336                 case MotionEvent.ACTION_MOVE:
    337                     // No need to cancel on vertical movement, ACTION_SCROLL will do that.
    338                     if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) {
    339                         clearClickedView((MonthWeekEventsView)v);
    340                     }
    341                     break;
    342                 default:
    343                     break;
    344             }
    345         }
    346         // Do not tell the frameworks we consumed the touch action so that fling actions can be
    347         // processed by the fragment.
    348         return false;
    349     }
    350 
    351     /**
    352      * This is here so we can identify events and process them
    353      */
    354     protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
    355         @Override
    356         public boolean onSingleTapUp(MotionEvent e) {
    357             return true;
    358         }
    359     }
    360 
    361     // Clear the visual cues of the click animation and related running code.
    362     private void clearClickedView(MonthWeekEventsView v) {
    363         mListView.removeCallbacks(mDoClick);
    364         synchronized(v) {
    365             v.clearClickedDay();
    366         }
    367         mClickedView = null;
    368     }
    369 
    370     // Perform the tap animation in a runnable to allow a delay before showing the tap color.
    371     // This is done to prevent a click animation when a fling is done.
    372     private final Runnable mDoClick = new Runnable() {
    373         @Override
    374         public void run() {
    375             if (mClickedView != null) {
    376                 synchronized(mClickedView) {
    377                     mClickedView.setClickedDay(mClickedXLocation);
    378                 }
    379                 mClickedView = null;
    380                 // This is a workaround , sometimes the top item on the listview doesn't refresh on
    381                 // invalidate, so this forces a re-draw.
    382                 mListView.invalidate();
    383             }
    384         }
    385     };
    386 
    387     // Performs the single tap operation: go to the tapped day.
    388     // This is done in a runnable to allow the click animation to finish before switching views
    389     private final Runnable mDoSingleTapUp = new Runnable() {
    390         @Override
    391         public void run() {
    392             if (mSingleTapUpView != null) {
    393                 Time day = mSingleTapUpView.getDayFromLocation(mClickedXLocation);
    394                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    395                     Log.d(TAG, "Touched day at Row=" + mSingleTapUpView.mWeek + " day=" + day.toString());
    396                 }
    397                 if (day != null) {
    398                     onDayTapped(day);
    399                 }
    400                 clearClickedView(mSingleTapUpView);
    401                 mSingleTapUpView = null;
    402             }
    403         }
    404     };
    405 }
    406