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