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