MonthByWeekFragment.java revision 41cdd1a43d80054c6a336585c40169e1c5538fda
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 17package com.android.calendar.month; 18 19import com.android.calendar.CalendarController; 20import com.android.calendar.CalendarController.EventInfo; 21import com.android.calendar.CalendarController.EventType; 22import com.android.calendar.CalendarController.ViewType; 23import com.android.calendar.Event; 24import com.android.calendar.R; 25import com.android.calendar.Utils; 26 27import android.app.Activity; 28import android.app.LoaderManager; 29import android.content.ContentUris; 30import android.content.CursorLoader; 31import android.content.Loader; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.net.Uri; 35import android.os.Bundle; 36import android.provider.CalendarContract.Attendees; 37import android.provider.CalendarContract.Calendars; 38import android.provider.CalendarContract.Instances; 39import android.text.format.DateUtils; 40import android.text.format.Time; 41import android.util.Log; 42import android.view.GestureDetector; 43import android.view.GestureDetector.SimpleOnGestureListener; 44import android.view.LayoutInflater; 45import android.view.MotionEvent; 46import android.view.View; 47import android.view.View.OnTouchListener; 48import android.view.ViewConfiguration; 49import android.view.ViewGroup; 50import android.widget.AbsListView; 51import android.widget.AbsListView.OnScrollListener; 52 53import java.util.ArrayList; 54import java.util.Calendar; 55import java.util.HashMap; 56 57public class MonthByWeekFragment extends SimpleDayPickerFragment implements 58 CalendarController.EventHandler, LoaderManager.LoaderCallbacks<Cursor>, OnScrollListener, 59 OnTouchListener { 60 private static final String TAG = "MonthFragment"; 61 62 // Selection and selection args for adding event queries 63 private static final String WHERE_CALENDARS_VISIBLE = Calendars.VISIBLE + "=1"; 64 private static final String INSTANCES_SORT_ORDER = Instances.START_DAY + "," 65 + Instances.START_MINUTE + "," + Instances.TITLE; 66 protected static boolean mShowDetailsInMonth = false; 67 68 protected float mMinimumTwoMonthFlingVelocity; 69 protected boolean mIsMiniMonth; 70 protected boolean mHideDeclined; 71 72 protected int mFirstLoadedJulianDay; 73 protected int mLastLoadedJulianDay; 74 75 private static final int WEEKS_BUFFER = 1; 76 // How long to wait after scroll stops before starting the loader 77 // Using scroll duration because scroll state changes don't update 78 // correctly when a scroll is triggered programmatically. 79 private static final int LOADER_DELAY = 200; 80 // The minimum time between requeries of the data if the db is 81 // changing 82 private static final int LOADER_THROTTLE_DELAY = 500; 83 84 private CursorLoader mLoader; 85 private Uri mEventUri; 86 private GestureDetector mGestureDetector; 87 private Time mDesiredDay = new Time(); 88 89 private volatile boolean mShouldLoad = true; 90 private boolean mUserScrolled = false; 91 92 private static float mScale = 0; 93 private static int SPACING_WEEK_NUMBER = 19; 94 95 // These define the behavior of the fling. Below MIN_VELOCITY_FOR_FLING, do the system fling 96 // behavior. Between MIN_VELOCITY_FOR_FLING and MULTIPLE_MONTH_VELOCITY_THRESHOLD, do one month 97 // fling. Above MULTIPLE_MONTH_VELOCITY_THRESHOLD, do multiple month flings according to the 98 // fling strength. When doing multiple month fling, the velocity is reduced by this threshold 99 // to prevent moving from one month fling to 4 months and above flings. 100 private static int MIN_VELOCITY_FOR_FLING = 750; 101 private static int MULTIPLE_MONTH_VELOCITY_THRESHOLD = 4000; 102 103 private Runnable mTZUpdater = new Runnable() { 104 @Override 105 public void run() { 106 String tz = Utils.getTimeZone(mContext, mTZUpdater); 107 mSelectedDay.timezone = tz; 108 mSelectedDay.normalize(true); 109 mTempTime.timezone = tz; 110 mFirstDayOfMonth.timezone = tz; 111 mFirstDayOfMonth.normalize(true); 112 mFirstVisibleDay.timezone = tz; 113 mFirstVisibleDay.normalize(true); 114 if (mAdapter != null) { 115 mAdapter.refresh(); 116 } 117 } 118 }; 119 120 121 private Runnable mUpdateLoader = new Runnable() { 122 @Override 123 public void run() { 124 synchronized (this) { 125 if (!mShouldLoad || mLoader == null) { 126 return; 127 } 128 // Stop any previous loads while we update the uri 129 stopLoader(); 130 131 // Start the loader again 132 mEventUri = updateUri(); 133 134 mLoader.setUri(mEventUri); 135 mLoader.startLoading(); 136 mLoader.onContentChanged(); 137 if (Log.isLoggable(TAG, Log.DEBUG)) { 138 Log.d(TAG, "Started loader with uri: " + mEventUri); 139 } 140 } 141 } 142 }; 143 144 /** 145 * Updates the uri used by the loader according to the current position of 146 * the listview. 147 * 148 * @return The new Uri to use 149 */ 150 private Uri updateUri() { 151 SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0); 152 if (child != null) { 153 int julianDay = child.getFirstJulianDay(); 154 mFirstLoadedJulianDay = julianDay; 155 } 156 // -1 to ensure we get all day events from any time zone 157 mTempTime.setJulianDay(mFirstLoadedJulianDay - 1); 158 long start = mTempTime.toMillis(true); 159 mLastLoadedJulianDay = mFirstLoadedJulianDay + (mNumWeeks + 2 * WEEKS_BUFFER) * 7; 160 // +1 to ensure we get all day events from any time zone 161 mTempTime.setJulianDay(mLastLoadedJulianDay + 1); 162 long end = mTempTime.toMillis(true); 163 164 // Create a new uri with the updated times 165 Uri.Builder builder = Instances.CONTENT_URI.buildUpon(); 166 ContentUris.appendId(builder, start); 167 ContentUris.appendId(builder, end); 168 return builder.build(); 169 } 170 171 protected String updateWhere() { 172 // TODO fix selection/selection args after b/3206641 is fixed 173 String where = WHERE_CALENDARS_VISIBLE; 174 if (mHideDeclined || !mShowDetailsInMonth) { 175 where += " AND " + Instances.SELF_ATTENDEE_STATUS + "!=" 176 + Attendees.ATTENDEE_STATUS_DECLINED; 177 } 178 return where; 179 } 180 181 private void stopLoader() { 182 synchronized (mUpdateLoader) { 183 mHandler.removeCallbacks(mUpdateLoader); 184 if (mLoader != null) { 185 mLoader.stopLoading(); 186 if (Log.isLoggable(TAG, Log.DEBUG)) { 187 Log.d(TAG, "Stopped loader from loading"); 188 } 189 } 190 } 191 } 192 193 class MonthGestureListener extends SimpleOnGestureListener { 194 @Override 195 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 196 float velocityY) { 197 198 // Small flings are just that, do not change the behavior 199 if (Math.abs(velocityY) < MIN_VELOCITY_FOR_FLING) { 200 return false; 201 } 202 203 // Below the threshold, fling one month. Above the threshold , fling according 204 // to the speed of the fling. 205 int monthsToJump; 206 if (Math.abs(velocityY) < MULTIPLE_MONTH_VELOCITY_THRESHOLD) { 207 if (velocityY < 0) { 208 monthsToJump = 1; 209 } else { 210 // value here is zero and not -1 since by the time the fling is detected 211 // the list moved back one month. 212 monthsToJump = 0; 213 } 214 } else { 215 if (velocityY < 0) { 216 monthsToJump = 1 - 217 (int)((velocityY + MULTIPLE_MONTH_VELOCITY_THRESHOLD) / 1000); 218 } else { 219 monthsToJump = -(int)((velocityY - MULTIPLE_MONTH_VELOCITY_THRESHOLD) / 1000); 220 } 221 } 222 223 // Get the day at the top right corner 224 int day = getUpperRightJulianDay(); 225 // Get the day of the first day of the next/previous month 226 // (according to scroll direction) 227 mTempTime.setJulianDay(day); 228 mTempTime.monthDay = 1; 229 mTempTime.month += monthsToJump; 230 long timeInMillis = mTempTime.normalize(true); 231 // Since each view is 7 days, round the target day up to make sure the scroll will be 232 // at least one view. 233 int scrollToDay = Time.getJulianDay(timeInMillis, mTempTime.gmtoff) + 234 ((monthsToJump > 0) ? 6 : 0); 235 int curPosition = mListView.getPositionForView(mListView.getChildAt(0)); 236 mListView.smoothScrollToPositionFromTop(curPosition + (scrollToDay - day) / 7, 237 LIST_TOP_OFFSET); 238 return true; 239 } 240 } 241 242 @Override 243 public void onAttach(Activity activity) { 244 super.onAttach(activity); 245 mTZUpdater.run(); 246 if (mAdapter != null) { 247 mAdapter.setSelectedDay(mSelectedDay); 248 } 249 250 mGestureDetector = new GestureDetector(activity, new MonthGestureListener()); 251 ViewConfiguration viewConfig = ViewConfiguration.get(activity); 252 mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity() / 2; 253 254 if (mScale == 0) { 255 Resources res = activity.getResources(); 256 mScale = res.getDisplayMetrics().density; 257 mShowDetailsInMonth = res.getBoolean(R.bool.show_details_in_month); 258 if (mScale != 1) { 259 SPACING_WEEK_NUMBER *= mScale; 260 MIN_VELOCITY_FOR_FLING *= mScale; 261 MULTIPLE_MONTH_VELOCITY_THRESHOLD *= mScale; 262 } 263 } 264 } 265 266 @Override 267 protected void setUpAdapter() { 268 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 269 mShowWeekNumber = Utils.getShowWeekNumber(mContext); 270 271 HashMap<String, Integer> weekParams = new HashMap<String, Integer>(); 272 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks); 273 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0); 274 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek); 275 weekParams.put(MonthByWeekAdapter.WEEK_PARAMS_IS_MINI, mIsMiniMonth ? 1 : 0); 276 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY, 277 Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff)); 278 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_DAYS_PER_WEEK, mDaysPerWeek); 279 if (mAdapter == null) { 280 mAdapter = new MonthByWeekAdapter(getActivity(), weekParams); 281 mAdapter.registerDataSetObserver(mObserver); 282 } else { 283 mAdapter.updateParams(weekParams); 284 } 285 mAdapter.notifyDataSetChanged(); 286 } 287 288 @Override 289 public View onCreateView( 290 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 291 View v; 292 if (mIsMiniMonth) { 293 v = inflater.inflate(R.layout.month_by_week, container, false); 294 } else { 295 v = inflater.inflate(R.layout.full_month_by_week, container, false); 296 } 297 mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names); 298 return v; 299 } 300 301 @Override 302 public void onActivityCreated(Bundle savedInstanceState) { 303 super.onActivityCreated(savedInstanceState); 304 mListView.setOnTouchListener(this); 305 if (!mIsMiniMonth) { 306 mListView.setBackgroundColor(getResources().getColor(R.color.month_bgcolor)); 307 } 308 mLoader = (CursorLoader) getLoaderManager().initLoader(0, null, this); 309 } 310 311 public MonthByWeekFragment() { 312 this(System.currentTimeMillis(), true); 313 } 314 315 public MonthByWeekFragment(long initialTime, boolean isMiniMonth) { 316 super(initialTime); 317 mIsMiniMonth = isMiniMonth; 318 } 319 320 @Override 321 protected void setUpHeader() { 322 if (mIsMiniMonth) { 323 super.setUpHeader(); 324 return; 325 } 326 327 mDayLabels = new String[7]; 328 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { 329 mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i, 330 DateUtils.LENGTH_MEDIUM).toUpperCase(); 331 } 332 } 333 334 // TODO 335 @Override 336 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 337 if (mIsMiniMonth) { 338 return null; 339 } 340 CursorLoader loader; 341 synchronized (mUpdateLoader) { 342 mFirstLoadedJulianDay = 343 Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff) 344 - (mNumWeeks * 7 / 2); 345 mEventUri = updateUri(); 346 String where = updateWhere(); 347 348 loader = new CursorLoader( 349 getActivity(), mEventUri, Event.EVENT_PROJECTION, where, 350 null /* WHERE_CALENDARS_SELECTED_ARGS */, INSTANCES_SORT_ORDER); 351 loader.setUpdateThrottle(LOADER_THROTTLE_DELAY); 352 } 353 if (Log.isLoggable(TAG, Log.DEBUG)) { 354 Log.d(TAG, "Returning new loader with uri: " + mEventUri); 355 } 356 return loader; 357 } 358 359 @Override 360 public void doResumeUpdates() { 361 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 362 mShowWeekNumber = Utils.getShowWeekNumber(mContext); 363 boolean prevHideDeclined = mHideDeclined; 364 mHideDeclined = Utils.getHideDeclinedEvents(mContext); 365 if (prevHideDeclined != mHideDeclined && mLoader != null) { 366 mLoader.setSelection(updateWhere()); 367 } 368 mDaysPerWeek = Utils.getDaysPerWeek(mContext); 369 updateHeader(); 370 mAdapter.setSelectedDay(mSelectedDay); 371 mTZUpdater.run(); 372 mTodayUpdater.run(); 373 goTo(mSelectedDay.toMillis(true), false, true, false); 374 } 375 376 @Override 377 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 378 synchronized (mUpdateLoader) { 379 if (Log.isLoggable(TAG, Log.DEBUG)) { 380 Log.d(TAG, "Found " + data.getCount() + " cursor entries for uri " + mEventUri); 381 } 382 CursorLoader cLoader = (CursorLoader) loader; 383 if (mEventUri == null) { 384 mEventUri = cLoader.getUri(); 385 } 386 if (cLoader.getUri().compareTo(mEventUri) != 0) { 387 // We've started a new query since this loader ran so ignore the 388 // result 389 return; 390 } 391 ArrayList<Event> events = new ArrayList<Event>(); 392 Event.buildEventsFromCursor( 393 events, data, mContext, mFirstLoadedJulianDay, mLastLoadedJulianDay); 394 ((MonthByWeekAdapter) mAdapter).setEvents(mFirstLoadedJulianDay, 395 mLastLoadedJulianDay - mFirstLoadedJulianDay + 1, events); 396 } 397 } 398 399 @Override 400 public void onLoaderReset(Loader<Cursor> loader) { 401 } 402 403 @Override 404 public void eventsChanged() { 405 // TODO remove this after b/3387924 is resolved 406 if (mLoader != null) { 407 mLoader.forceLoad(); 408 } 409 } 410 411 @Override 412 public long getSupportedEventTypes() { 413 return EventType.GO_TO | EventType.EVENTS_CHANGED; 414 } 415 416 @Override 417 public void handleEvent(EventInfo event) { 418 if (event.eventType == EventType.GO_TO) { 419 boolean animate = true; 420 if (mDaysPerWeek * mNumWeeks * 2 < Math.abs( 421 Time.getJulianDay(event.selectedTime.toMillis(true), event.selectedTime.gmtoff) 422 - Time.getJulianDay(mFirstVisibleDay.toMillis(true), mFirstVisibleDay.gmtoff) 423 - mDaysPerWeek * mNumWeeks / 2)) { 424 animate = false; 425 } 426 mDesiredDay.set(event.selectedTime); 427 mDesiredDay.normalize(true); 428 boolean animateToday = (event.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0; 429 boolean delayAnimation = goTo(event.selectedTime.toMillis(true), animate, true, false); 430 if (animateToday) { 431 // If we need to flash today start the animation after any 432 // movement from listView has ended. 433 mHandler.postDelayed(new Runnable() { 434 @Override 435 public void run() { 436 ((MonthByWeekAdapter) mAdapter).animateToday(); 437 mAdapter.notifyDataSetChanged(); 438 } 439 }, delayAnimation ? GOTO_SCROLL_DURATION : 0); 440 } 441 } else if (event.eventType == EventType.EVENTS_CHANGED) { 442 eventsChanged(); 443 } 444 } 445 446 @Override 447 protected void setMonthDisplayed(Time time, boolean updateHighlight) { 448 super.setMonthDisplayed(time, updateHighlight); 449 if (!mIsMiniMonth) { 450 boolean useSelected = false; 451 if (time.year == mDesiredDay.year && time.month == mDesiredDay.month) { 452 mSelectedDay.set(mDesiredDay); 453 mAdapter.setSelectedDay(mDesiredDay); 454 useSelected = true; 455 } else { 456 mSelectedDay.set(time); 457 mAdapter.setSelectedDay(time); 458 } 459 CalendarController controller = CalendarController.getInstance(mContext); 460 if (mSelectedDay.minute >= 30) { 461 mSelectedDay.minute = 30; 462 } else { 463 mSelectedDay.minute = 0; 464 } 465 long newTime = mSelectedDay.normalize(true); 466 if (newTime != controller.getTime() && mUserScrolled) { 467 long offset = useSelected ? 0 : DateUtils.WEEK_IN_MILLIS * mNumWeeks / 3; 468 controller.setTime(newTime + offset); 469 } 470 controller.sendEvent(this, EventType.UPDATE_TITLE, time, time, time, -1, 471 ViewType.CURRENT, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY 472 | DateUtils.FORMAT_SHOW_YEAR, null, null); 473 } 474 } 475 476 @Override 477 public void onScrollStateChanged(AbsListView view, int scrollState) { 478 479 synchronized (mUpdateLoader) { 480 if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) { 481 mShouldLoad = false; 482 stopLoader(); 483 mDesiredDay.setToNow(); 484 } else { 485 mHandler.removeCallbacks(mUpdateLoader); 486 mShouldLoad = true; 487 mHandler.postDelayed(mUpdateLoader, LOADER_DELAY); 488 } 489 } 490 if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 491 mUserScrolled = true; 492 } 493 494 mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); 495 } 496 497 @Override 498 public boolean onTouch(View v, MotionEvent event) { 499 mDesiredDay.setToNow(); 500 return mGestureDetector.onTouchEvent(event); 501 // TODO post a cleanup to push us back onto the grid if something went 502 // wrong in a scroll such as the user stopping the view but not 503 // scrolling 504 } 505 506} 507