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