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