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