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 }