/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calendar.month; import android.content.Context; import android.content.res.Configuration; import android.os.Handler; import android.os.Message; import android.text.format.Time; import android.util.Log; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.AbsListView.LayoutParams; import com.android.calendar.CalendarController; import com.android.calendar.CalendarController.EventType; import com.android.calendar.CalendarController.ViewType; import com.android.calendar.Event; import com.android.calendar.R; import com.android.calendar.Utils; import java.util.ArrayList; import java.util.HashMap; public class MonthByWeekAdapter extends SimpleWeeksAdapter { private static final String TAG = "MonthByWeekAdapter"; public static final String WEEK_PARAMS_IS_MINI = "mini_month"; protected static int DEFAULT_QUERY_DAYS = 7 * 8; // 8 weeks private static final long ANIMATE_TODAY_TIMEOUT = 1000; protected CalendarController mController; protected String mHomeTimeZone; protected Time mTempTime; protected Time mToday; protected int mFirstJulianDay; protected int mQueryDays; protected boolean mIsMiniMonth = true; protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE; private final boolean mShowAgendaWithMonth; protected ArrayList<ArrayList<Event>> mEventDayList = new ArrayList<ArrayList<Event>>(); protected ArrayList<Event> mEvents = null; private boolean mAnimateToday = false; private long mAnimateTime = 0; private Handler mEventDialogHandler; MonthWeekEventsView mClickedView; MonthWeekEventsView mSingleTapUpView; MonthWeekEventsView mLongClickedView; float mClickedXLocation; // Used to find which day was clicked long mClickTime; // Used to calculate minimum click animation time // Used to insure minimal time for seeing the click animation before switching views private static final int mOnTapDelay = 100; // Minimal time for a down touch action before stating the click animation, this insures that // there is no click animation on flings private static int mOnDownDelay; private static int mTotalClickDelay; // Minimal distance to move the finger in order to cancel the click animation private static float mMovedPixelToCancel; public MonthByWeekAdapter(Context context, HashMap<String, Integer> params, Handler handler) { super(context, params); mEventDialogHandler = handler; if (params.containsKey(WEEK_PARAMS_IS_MINI)) { mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0; } mShowAgendaWithMonth = Utils.getConfigBool(context, R.bool.show_agenda_with_month); ViewConfiguration vc = ViewConfiguration.get(context); mOnDownDelay = ViewConfiguration.getTapTimeout(); mMovedPixelToCancel = vc.getScaledTouchSlop(); mTotalClickDelay = mOnDownDelay + mOnTapDelay; } public void animateToday() { mAnimateToday = true; mAnimateTime = System.currentTimeMillis(); } @Override protected void init() { super.init(); mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); mController = CalendarController.getInstance(mContext); mHomeTimeZone = Utils.getTimeZone(mContext, null); mSelectedDay.switchTimezone(mHomeTimeZone); mToday = new Time(mHomeTimeZone); mToday.setToNow(); mTempTime = new Time(mHomeTimeZone); } private void updateTimeZones() { mSelectedDay.timezone = mHomeTimeZone; mSelectedDay.normalize(true); mToday.timezone = mHomeTimeZone; mToday.setToNow(); mTempTime.switchTimezone(mHomeTimeZone); } @Override public void setSelectedDay(Time selectedTime) { mSelectedDay.set(selectedTime); long millis = mSelectedDay.normalize(true); mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay( Time.getJulianDay(millis, mSelectedDay.gmtoff), mFirstDayOfWeek); notifyDataSetChanged(); } public void setEvents(int firstJulianDay, int numDays, ArrayList<Event> events) { if (mIsMiniMonth) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Attempted to set events for mini view. Events only supported in full" + " view."); } return; } mEvents = events; mFirstJulianDay = firstJulianDay; mQueryDays = numDays; // Create a new list, this is necessary since the weeks are referencing // pieces of the old list ArrayList<ArrayList<Event>> eventDayList = new ArrayList<ArrayList<Event>>(); for (int i = 0; i < numDays; i++) { eventDayList.add(new ArrayList<Event>()); } if (events == null || events.size() == 0) { if(Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "No events. Returning early--go schedule something fun."); } mEventDayList = eventDayList; refresh(); return; } // Compute the new set of days with events for (Event event : events) { int startDay = event.startDay - mFirstJulianDay; int endDay = event.endDay - mFirstJulianDay + 1; if (startDay < numDays || endDay >= 0) { if (startDay < 0) { startDay = 0; } if (startDay > numDays) { continue; } if (endDay < 0) { continue; } if (endDay > numDays) { endDay = numDays; } for (int j = startDay; j < endDay; j++) { eventDayList.get(j).add(event); } } } if(Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Processed " + events.size() + " events."); } mEventDayList = eventDayList; refresh(); } @SuppressWarnings("unchecked") @Override public View getView(int position, View convertView, ViewGroup parent) { if (mIsMiniMonth) { return super.getView(position, convertView, parent); } MonthWeekEventsView v; LayoutParams params = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); HashMap<String, Integer> drawingParams = null; boolean isAnimatingToday = false; if (convertView != null) { v = (MonthWeekEventsView) convertView; // Checking updateToday uses the current params instead of the new // params, so this is assuming the view is relatively stable if (mAnimateToday && v.updateToday(mSelectedDay.timezone)) { long currentTime = System.currentTimeMillis(); // If it's been too long since we tried to start the animation // don't show it. This can happen if the user stops a scroll // before reaching today. if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) { mAnimateToday = false; mAnimateTime = 0; } else { isAnimatingToday = true; // There is a bug that causes invalidates to not work some // of the time unless we recreate the view. v = new MonthWeekEventsView(mContext); } } else { drawingParams = (HashMap<String, Integer>) v.getTag(); } } else { v = new MonthWeekEventsView(mContext); } if (drawingParams == null) { drawingParams = new HashMap<String, Integer>(); } drawingParams.clear(); v.setLayoutParams(params); v.setClickable(true); v.setOnTouchListener(this); int selectedDay = -1; if (mSelectedWeek == position) { selectedDay = mSelectedDay.weekDay; } drawingParams.put(SimpleWeekView.VIEW_PARAMS_HEIGHT, (parent.getHeight() + parent.getTop()) / mNumWeeks); drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay); drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, mShowWeekNumber ? 1 : 0); drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek); drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek); drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position); drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth); drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation); if (isAnimatingToday) { drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1); mAnimateToday = false; } v.setWeekParams(drawingParams, mSelectedDay.timezone); sendEventsToView(v); return v; } private void sendEventsToView(MonthWeekEventsView v) { if (mEventDayList.size() == 0) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "No events loaded, did not pass any events to view."); } v.setEvents(null, null); return; } int viewJulianDay = v.getFirstJulianDay(); int start = viewJulianDay - mFirstJulianDay; int end = start + v.mNumDays; if (start < 0 || end > mEventDayList.size()) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Week is outside range of loaded events. viewStart: " + viewJulianDay + " eventsStart: " + mFirstJulianDay); } v.setEvents(null, null); return; } v.setEvents(mEventDayList.subList(start, end), mEvents); } @Override protected void refresh() { mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); mShowWeekNumber = Utils.getShowWeekNumber(mContext); mHomeTimeZone = Utils.getTimeZone(mContext, null); mOrientation = mContext.getResources().getConfiguration().orientation; updateTimeZones(); notifyDataSetChanged(); } @Override protected void onDayTapped(Time day) { setDayParameters(day); if (mShowAgendaWithMonth || mIsMiniMonth) { // If agenda view is visible with month view , refresh the views // with the selected day's info mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); } else { // Else , switch to the detailed view mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, ViewType.DETAIL, CalendarController.EXTRA_GOTO_DATE | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null); } } private void setDayParameters(Time day) { day.timezone = mHomeTimeZone; Time currTime = new Time(mHomeTimeZone); currTime.set(mController.getTime()); day.hour = currTime.hour; day.minute = currTime.minute; day.allDay = false; day.normalize(true); } @Override public boolean onTouch(View v, MotionEvent event) { if (!(v instanceof MonthWeekEventsView)) { return super.onTouch(v, event); } int action = event.getAction(); // Event was tapped - switch to the detailed view making sure the click animation // is done first. if (mGestureDetector.onTouchEvent(event)) { mSingleTapUpView = (MonthWeekEventsView) v; long delay = System.currentTimeMillis() - mClickTime; // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms mListView.postDelayed(mDoSingleTapUp, delay > mTotalClickDelay ? 0 : mTotalClickDelay - delay); return true; } else { // Animate a click - on down: show the selected day in the "clicked" color. // On Up/scroll/move/cancel: hide the "clicked" color. switch (action) { case MotionEvent.ACTION_DOWN: mClickedView = (MonthWeekEventsView)v; mClickedXLocation = event.getX(); mClickTime = System.currentTimeMillis(); mListView.postDelayed(mDoClick, mOnDownDelay); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_SCROLL: case MotionEvent.ACTION_CANCEL: clearClickedView((MonthWeekEventsView)v); break; case MotionEvent.ACTION_MOVE: // No need to cancel on vertical movement, ACTION_SCROLL will do that. if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) { clearClickedView((MonthWeekEventsView)v); } break; default: break; } } // Do not tell the frameworks we consumed the touch action so that fling actions can be // processed by the fragment. return false; } /** * This is here so we can identify events and process them */ protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapUp(MotionEvent e) { return true; } @Override public void onLongPress(MotionEvent e) { if (mLongClickedView != null) { Time day = mLongClickedView.getDayFromLocation(mClickedXLocation); if (day != null) { mLongClickedView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); Message message = new Message(); message.obj = day; mEventDialogHandler.sendMessage(message); } mLongClickedView.clearClickedDay(); mLongClickedView = null; } } } // Clear the visual cues of the click animation and related running code. private void clearClickedView(MonthWeekEventsView v) { mListView.removeCallbacks(mDoClick); synchronized(v) { v.clearClickedDay(); } mClickedView = null; } // Perform the tap animation in a runnable to allow a delay before showing the tap color. // This is done to prevent a click animation when a fling is done. private final Runnable mDoClick = new Runnable() { @Override public void run() { if (mClickedView != null) { synchronized(mClickedView) { mClickedView.setClickedDay(mClickedXLocation); } mLongClickedView = mClickedView; mClickedView = null; // This is a workaround , sometimes the top item on the listview doesn't refresh on // invalidate, so this forces a re-draw. mListView.invalidate(); } } }; // Performs the single tap operation: go to the tapped day. // This is done in a runnable to allow the click animation to finish before switching views private final Runnable mDoSingleTapUp = new Runnable() { @Override public void run() { if (mSingleTapUpView != null) { Time day = mSingleTapUpView.getDayFromLocation(mClickedXLocation); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Touched day at Row=" + mSingleTapUpView.mWeek + " day=" + day.toString()); } if (day != null) { onDayTapped(day); } clearClickedView(mSingleTapUpView); mSingleTapUpView = null; } } }; }