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