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 MonthByWeekAdapter(Context context, HashMap<String, Integer> params)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 animateToday()90 public void animateToday() { 91 mAnimateToday = true; 92 mAnimateTime = System.currentTimeMillis(); 93 } 94 95 @Override init()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 updateTimeZones()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 setSelectedDay(Time selectedTime)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 setEvents(int firstJulianDay, int numDays, ArrayList<Event> events)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 getView(int position, View convertView, ViewGroup parent)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 sendEventsToView(MonthWeekEventsView v)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 refresh()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 onDayTapped(Time day)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 ViewType.DETAIL, 298 CalendarController.EXTRA_GOTO_DATE 299 | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null); 300 } 301 } 302 303 @Override onTouch(View v, MotionEvent event)304 public boolean onTouch(View v, MotionEvent event) { 305 if (!(v instanceof MonthWeekEventsView)) { 306 return super.onTouch(v, event); 307 } 308 309 int action = event.getAction(); 310 311 // Event was tapped - switch to the detailed view making sure the click animation 312 // is done first. 313 if (mGestureDetector.onTouchEvent(event)) { 314 mSingleTapUpView = (MonthWeekEventsView) v; 315 long delay = System.currentTimeMillis() - mClickTime; 316 // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms 317 mListView.postDelayed(mDoSingleTapUp, 318 delay > mTotalClickDelay ? 0 : mTotalClickDelay - delay); 319 return true; 320 } else { 321 // Animate a click - on down: show the selected day in the "clicked" color. 322 // On Up/scroll/move/cancel: hide the "clicked" color. 323 switch (action) { 324 case MotionEvent.ACTION_DOWN: 325 mClickedView = (MonthWeekEventsView)v; 326 mClickedXLocation = event.getX(); 327 mClickTime = System.currentTimeMillis(); 328 mListView.postDelayed(mDoClick, mOnDownDelay); 329 break; 330 case MotionEvent.ACTION_UP: 331 case MotionEvent.ACTION_SCROLL: 332 case MotionEvent.ACTION_CANCEL: 333 clearClickedView((MonthWeekEventsView)v); 334 break; 335 case MotionEvent.ACTION_MOVE: 336 // No need to cancel on vertical movement, ACTION_SCROLL will do that. 337 if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) { 338 clearClickedView((MonthWeekEventsView)v); 339 } 340 break; 341 default: 342 break; 343 } 344 } 345 // Do not tell the frameworks we consumed the touch action so that fling actions can be 346 // processed by the fragment. 347 return false; 348 } 349 350 /** 351 * This is here so we can identify events and process them 352 */ 353 protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 354 @Override onSingleTapUp(MotionEvent e)355 public boolean onSingleTapUp(MotionEvent e) { 356 return true; 357 } 358 } 359 360 // Clear the visual cues of the click animation and related running code. clearClickedView(MonthWeekEventsView v)361 private void clearClickedView(MonthWeekEventsView v) { 362 mListView.removeCallbacks(mDoClick); 363 synchronized(v) { 364 v.clearClickedDay(); 365 } 366 mClickedView = null; 367 } 368 369 // Perform the tap animation in a runnable to allow a delay before showing the tap color. 370 // This is done to prevent a click animation when a fling is done. 371 private final Runnable mDoClick = new Runnable() { 372 @Override 373 public void run() { 374 if (mClickedView != null) { 375 synchronized(mClickedView) { 376 mClickedView.setClickedDay(mClickedXLocation); 377 } 378 mClickedView = null; 379 // This is a workaround , sometimes the top item on the listview doesn't refresh on 380 // invalidate, so this forces a re-draw. 381 mListView.invalidate(); 382 } 383 } 384 }; 385 386 // Performs the single tap operation: go to the tapped day. 387 // This is done in a runnable to allow the click animation to finish before switching views 388 private final Runnable mDoSingleTapUp = new Runnable() { 389 @Override 390 public void run() { 391 if (mSingleTapUpView != null) { 392 Time day = mSingleTapUpView.getDayFromLocation(mClickedXLocation); 393 if (Log.isLoggable(TAG, Log.DEBUG)) { 394 Log.d(TAG, "Touched day at Row=" + mSingleTapUpView.mWeek + " day=" + day.toString()); 395 } 396 if (day != null) { 397 onDayTapped(day); 398 } 399 clearClickedView(mSingleTapUpView); 400 mSingleTapUpView = null; 401 } 402 } 403 }; 404 } 405