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