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