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