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.content.Context
19 import android.content.res.Configuration
20 import android.os.Handler
21 import android.os.Message
22 import android.text.format.Time
23 import android.util.Log
24 import android.view.GestureDetector
25 import android.view.HapticFeedbackConstants
26 import android.view.MotionEvent
27 import android.view.View
28 import android.view.ViewConfiguration
29 import android.view.ViewGroup
30 import android.widget.AbsListView.LayoutParams
31 import com.android.calendar.CalendarController
32 import com.android.calendar.CalendarController.EventType
33 import com.android.calendar.CalendarController.ViewType
34 import com.android.calendar.Event
35 import com.android.calendar.R
36 import com.android.calendar.Utils
37 import java.util.ArrayList
38 import java.util.HashMap
39 
40 class MonthByWeekAdapter(context: Context?, params: HashMap<String?, Int?>) :
41     SimpleWeeksAdapter(context as Context, params) {
42     protected var mController: CalendarController? = null
43     protected var mHomeTimeZone: String? = null
44     protected var mTempTime: Time? = null
45     protected var mToday: Time? = null
46     protected var mFirstJulianDay = 0
47     protected var mQueryDays = 0
48     protected var mIsMiniMonth = true
49     protected var mOrientation: Int = Configuration.ORIENTATION_LANDSCAPE
50     private val mShowAgendaWithMonth: Boolean
51     protected var mEventDayList: ArrayList<ArrayList<Event>> = ArrayList<ArrayList<Event>>()
52     protected var mEvents: ArrayList<Event>? = null
53     private var mAnimateToday = false
54     private var mAnimateTime: Long = 0
55     private val mEventDialogHandler: Handler? = null
56     var mClickedView: MonthWeekEventsView? = null
57     var mSingleTapUpView: MonthWeekEventsView? = null
58     var mLongClickedView: MonthWeekEventsView? = null
59     var mClickedXLocation = 0f // Used to find which day was clicked
60     var mClickTime: Long = 0 // Used to calculate minimum click animation time
61 
animateTodaynull62     fun animateToday() {
63         mAnimateToday = true
64         mAnimateTime = System.currentTimeMillis()
65     }
66 
67     @Override
initnull68     protected override fun init() {
69         super.init()
70         mGestureDetector = GestureDetector(mContext, CalendarGestureListener())
71         mController = CalendarController.getInstance(mContext)
72         mHomeTimeZone = Utils.getTimeZone(mContext, null)
73         mSelectedDay?.switchTimezone(mHomeTimeZone)
74         mToday = Time(mHomeTimeZone)
75         mToday?.setToNow()
76         mTempTime = Time(mHomeTimeZone)
77     }
78 
updateTimeZonesnull79     private fun updateTimeZones() {
80         mSelectedDay!!.timezone = mHomeTimeZone
81         mSelectedDay?.normalize(true)
82         mToday!!.timezone = mHomeTimeZone
83         mToday?.setToNow()
84         mTempTime?.switchTimezone(mHomeTimeZone)
85     }
86 
87     @Override
setSelectedDaynull88     override fun setSelectedDay(selectedTime: Time?) {
89         mSelectedDay?.set(selectedTime)
90         val millis: Long = mSelectedDay!!.normalize(true)
91         mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay(
92             Time.getJulianDay(millis, mSelectedDay!!.gmtoff), mFirstDayOfWeek
93         )
94         notifyDataSetChanged()
95     }
96 
setEventsnull97     fun setEvents(firstJulianDay: Int, numDays: Int, events: ArrayList<Event>?) {
98         if (mIsMiniMonth) {
99             if (Log.isLoggable(TAG, Log.ERROR)) {
100                 Log.e(
101                     TAG, "Attempted to set events for mini view. Events only supported in full" +
102                         " view."
103                 )
104             }
105             return
106         }
107         mEvents = events
108         mFirstJulianDay = firstJulianDay
109         mQueryDays = numDays
110         // Create a new list, this is necessary since the weeks are referencing
111         // pieces of the old list
112         val eventDayList: ArrayList<ArrayList<Event>> = ArrayList<ArrayList<Event>>()
113         for (i in 0 until numDays) {
114             eventDayList.add(ArrayList<Event>())
115         }
116         if (events == null || events.size == 0) {
117             if (Log.isLoggable(TAG, Log.DEBUG)) {
118                 Log.d(TAG, "No events. Returning early--go schedule something fun.")
119             }
120             mEventDayList = eventDayList
121             refresh()
122             return
123         }
124 
125         // Compute the new set of days with events
126         for (event in events) {
127             var startDay: Int = event.startDay - mFirstJulianDay
128             var endDay: Int = event.endDay - mFirstJulianDay + 1
129             if (startDay < numDays || endDay >= 0) {
130                 if (startDay < 0) {
131                     startDay = 0
132                 }
133                 if (startDay > numDays) {
134                     continue
135                 }
136                 if (endDay < 0) {
137                     continue
138                 }
139                 if (endDay > numDays) {
140                     endDay = numDays
141                 }
142                 for (j in startDay until endDay) {
143                     eventDayList.get(j).add(event)
144                 }
145             }
146         }
147         if (Log.isLoggable(TAG, Log.DEBUG)) {
148             Log.d(TAG, "Processed " + events.size.toString() + " events.")
149         }
150         mEventDayList = eventDayList
151         refresh()
152     }
153 
154     @SuppressWarnings("unchecked")
155     @Override
getViewnull156     override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
157         if (mIsMiniMonth) {
158             return super.getView(position, convertView, parent)
159         }
160         var v: MonthWeekEventsView
161         val params = LayoutParams(
162             LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
163         )
164         var drawingParams: HashMap<String?, Int?>? = null
165         var isAnimatingToday = false
166         if (convertView != null) {
167             v = convertView as MonthWeekEventsView
168             // Checking updateToday uses the current params instead of the new
169             // params, so this is assuming the view is relatively stable
170             if (mAnimateToday && v.updateToday(mSelectedDay!!.timezone)) {
171                 val currentTime: Long = System.currentTimeMillis()
172                 // If it's been too long since we tried to start the animation
173                 // don't show it. This can happen if the user stops a scroll
174                 // before reaching today.
175                 if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) {
176                     mAnimateToday = false
177                     mAnimateTime = 0
178                 } else {
179                     isAnimatingToday = true
180                     // There is a bug that causes invalidates to not work some
181                     // of the time unless we recreate the view.
182                     v = MonthWeekEventsView(mContext)
183                 }
184             } else {
185                 drawingParams = v.getTag() as HashMap<String?, Int?>
186             }
187         } else {
188             v = MonthWeekEventsView(mContext)
189         }
190         if (drawingParams == null) {
191             drawingParams = HashMap<String?, Int?>()
192         }
193         drawingParams.clear()
194         v.setLayoutParams(params)
195         v.setClickable(true)
196         v.setOnTouchListener(this)
197         var selectedDay = -1
198         if (mSelectedWeek === position) {
199             selectedDay = mSelectedDay!!.weekDay
200         }
201         drawingParams.put(
202             SimpleWeekView.VIEW_PARAMS_HEIGHT,
203             (parent.getHeight() + parent.getTop()) / mNumWeeks
204         )
205         drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay)
206         drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, if (mShowWeekNumber) 1 else 0)
207         drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek)
208         drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek)
209         drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position)
210         drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth)
211         drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation)
212         if (isAnimatingToday) {
213             drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1)
214             mAnimateToday = false
215         }
216         v.setWeekParams(drawingParams, mSelectedDay!!.timezone)
217         return v
218     }
219 
220     @Override
refreshnull221     internal override fun refresh() {
222         mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext)
223         mShowWeekNumber = Utils.getShowWeekNumber(mContext)
224         mHomeTimeZone = Utils.getTimeZone(mContext, null)
225         mOrientation = mContext.getResources().getConfiguration().orientation
226         updateTimeZones()
227         notifyDataSetChanged()
228     }
229 
230     @Override
onDayTappednull231     protected override fun onDayTapped(day: Time) {
232         setDayParameters(day)
233         if (mShowAgendaWithMonth || mIsMiniMonth) {
234             // If agenda view is visible with month view , refresh the views
235             // with the selected day's info
236             mController?.sendEvent(
237                 mContext as Object?, EventType.GO_TO, day, day, -1,
238                 ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null
239             )
240         } else {
241             // Else , switch to the detailed view
242             mController?.sendEvent(
243                 mContext as Object?, EventType.GO_TO, day, day, -1,
244                 ViewType.DETAIL, CalendarController.EXTRA_GOTO_DATE
245                     or CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null
246             )
247         }
248     }
249 
setDayParametersnull250     private fun setDayParameters(day: Time) {
251         day.timezone = mHomeTimeZone
252         val currTime = Time(mHomeTimeZone)
253         currTime.set(mController!!.time as Long)
254         day.hour = currTime.hour
255         day.minute = currTime.minute
256         day.allDay = false
257         day.normalize(true)
258     }
259 
260     @Override
onTouchnull261     override fun onTouch(v: View, event: MotionEvent): Boolean {
262         if (v !is MonthWeekEventsView) {
263             return super.onTouch(v, event)
264         }
265         val action: Int = event.getAction()
266 
267         // Event was tapped - switch to the detailed view making sure the click animation
268         // is done first.
269         if (mGestureDetector!!.onTouchEvent(event)) {
270             mSingleTapUpView = v as MonthWeekEventsView?
271             val delay: Long = System.currentTimeMillis() - mClickTime
272             // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms
273             mListView?.postDelayed(
274                 mDoSingleTapUp,
275                 if (delay > mTotalClickDelay) 0 else mTotalClickDelay - delay
276             )
277             return true
278         } else {
279             // Animate a click - on down: show the selected day in the "clicked" color.
280             // On Up/scroll/move/cancel: hide the "clicked" color.
281             when (action) {
282                 MotionEvent.ACTION_DOWN -> {
283                     mClickedView = v as MonthWeekEventsView
284                     mClickedXLocation = event.getX()
285                     mClickTime = System.currentTimeMillis()
286                     mListView?.postDelayed(mDoClick, mOnDownDelay.toLong())
287                 }
288                 MotionEvent.ACTION_UP, MotionEvent.ACTION_SCROLL, MotionEvent.ACTION_CANCEL ->
289                     clearClickedView(
290                     v as MonthWeekEventsView?
291                 )
292                 MotionEvent.ACTION_MOVE -> // No need to cancel on vertical movement,
293                     // ACTION_SCROLL will do that.
294                     if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) {
295                         clearClickedView(v as MonthWeekEventsView?)
296                     }
297                 else -> {
298                 }
299             }
300         }
301         // Do not tell the frameworks we consumed the touch action so that fling actions can be
302         // processed by the fragment.
303         return false
304     }
305 
306     /**
307      * This is here so we can identify events and process them
308      */
309     protected inner class CalendarGestureListener : GestureDetector.SimpleOnGestureListener() {
310         @Override
onSingleTapUpnull311         override fun onSingleTapUp(e: MotionEvent): Boolean {
312             return true
313         }
314 
315         @Override
onLongPressnull316         override fun onLongPress(e: MotionEvent) {
317             if (mLongClickedView != null) {
318                 val day: Time? = mLongClickedView?.getDayFromLocation(mClickedXLocation)
319                 if (day != null) {
320                     mLongClickedView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
321                     val message = Message()
322                     message.obj = day
323                 }
324                 mLongClickedView?.clearClickedDay()
325                 mLongClickedView = null
326             }
327         }
328     }
329 
330     // Clear the visual cues of the click animation and related running code.
clearClickedViewnull331     private fun clearClickedView(v: MonthWeekEventsView?) {
332         mListView?.removeCallbacks(mDoClick)
333         synchronized(v as Any) { v.clearClickedDay() }
334         mClickedView = null
335     }
336 
337     // Perform the tap animation in a runnable to allow a delay before showing the tap color.
338     // This is done to prevent a click animation when a fling is done.
339     private val mDoClick: Runnable = object : Runnable {
340         @Override
runnull341         override fun run() {
342             if (mClickedView != null) {
343                 synchronized(mClickedView as MonthWeekEventsView) {
344                     mClickedView?.setClickedDay(mClickedXLocation) }
345                 mLongClickedView = mClickedView
346                 mClickedView = null
347                 // This is a workaround , sometimes the top item on the listview doesn't refresh on
348                 // invalidate, so this forces a re-draw.
349                 mListView?.invalidate()
350             }
351         }
352     }
353 
354     // Performs the single tap operation: go to the tapped day.
355     // This is done in a runnable to allow the click animation to finish before switching views
356     private val mDoSingleTapUp: Runnable = object : Runnable {
357         @Override
runnull358         override fun run() {
359             if (mSingleTapUpView != null) {
360                 val day: Time? = mSingleTapUpView?.getDayFromLocation(mClickedXLocation)
361                 if (Log.isLoggable(TAG, Log.DEBUG)) {
362                     Log.d(
363                         TAG,
364                         "Touched day at Row=" + mSingleTapUpView?.mWeek?.toString() +
365                             " day=" + day?.toString()
366                     )
367                 }
368                 if (day != null) {
369                     onDayTapped(day)
370                 }
371                 clearClickedView(mSingleTapUpView)
372                 mSingleTapUpView = null
373             }
374         }
375     }
376 
377     companion object {
378         private const val TAG = "MonthByWeekAdapter"
379         const val WEEK_PARAMS_IS_MINI = "mini_month"
380         protected var DEFAULT_QUERY_DAYS = 7 * 8 // 8 weeks
381         private const val ANIMATE_TODAY_TIMEOUT: Long = 1000
382 
383         // Used to insure minimal time for seeing the click animation before switching views
384         private const val mOnTapDelay = 100
385 
386         // Minimal time for a down touch action before stating the click animation, this ensures
387         // that there is no click animation on flings
388         private var mOnDownDelay: Int = 0
389         private var mTotalClickDelay: Int = 0
390 
391         // Minimal distance to move the finger in order to cancel the click animation
392         private var mMovedPixelToCancel: Float = 0f
393     }
394 
395     init {
396         if (params.containsKey(WEEK_PARAMS_IS_MINI)) {
397             mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0
398         }
399         mShowAgendaWithMonth = Utils.getConfigBool(context as Context,
400             R.bool.show_agenda_with_month)
401         val vc: ViewConfiguration = ViewConfiguration.get(context)
402         mOnDownDelay = ViewConfiguration.getTapTimeout()
403         mMovedPixelToCancel = vc.getScaledTouchSlop().toFloat()
404         mTotalClickDelay = mOnDownDelay + mOnTapDelay
405     }
406 }