summaryrefslogtreecommitdiff
path: root/src/com/android/calendar/month/MonthByWeekAdapter.kt
blob: c67b3562663d3fd62843bc32a9681f0dbee2eb96 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.calendar.month

import android.content.Context
import android.content.res.Configuration
import android.os.Handler
import android.os.Message
import android.text.format.Time
import android.util.Log
import android.view.GestureDetector
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.widget.AbsListView.LayoutParams
import com.android.calendar.CalendarController
import com.android.calendar.CalendarController.EventType
import com.android.calendar.CalendarController.ViewType
import com.android.calendar.Event
import com.android.calendar.R
import com.android.calendar.Utils
import java.util.ArrayList
import java.util.HashMap

class MonthByWeekAdapter(context: Context?, params: HashMap<String?, Int?>) :
    SimpleWeeksAdapter(context as Context, params) {
    protected var mController: CalendarController? = null
    protected var mHomeTimeZone: String? = null
    protected var mTempTime: Time? = null
    protected var mToday: Time? = null
    protected var mFirstJulianDay = 0
    protected var mQueryDays = 0
    protected var mIsMiniMonth = true
    protected var mOrientation: Int = Configuration.ORIENTATION_LANDSCAPE
    private val mShowAgendaWithMonth: Boolean
    protected var mEventDayList: ArrayList<ArrayList<Event>> = ArrayList<ArrayList<Event>>()
    protected var mEvents: ArrayList<Event>? = null
    private var mAnimateToday = false
    private var mAnimateTime: Long = 0
    private val mEventDialogHandler: Handler? = null
    var mClickedView: MonthWeekEventsView? = null
    var mSingleTapUpView: MonthWeekEventsView? = null
    var mLongClickedView: MonthWeekEventsView? = null
    var mClickedXLocation = 0f // Used to find which day was clicked
    var mClickTime: Long = 0 // Used to calculate minimum click animation time

    fun animateToday() {
        mAnimateToday = true
        mAnimateTime = System.currentTimeMillis()
    }

    @Override
    protected override fun init() {
        super.init()
        mGestureDetector = GestureDetector(mContext, CalendarGestureListener())
        mController = CalendarController.getInstance(mContext)
        mHomeTimeZone = Utils.getTimeZone(mContext, null)
        mSelectedDay?.switchTimezone(mHomeTimeZone)
        mToday = Time(mHomeTimeZone)
        mToday?.setToNow()
        mTempTime = Time(mHomeTimeZone)
    }

    private fun updateTimeZones() {
        mSelectedDay!!.timezone = mHomeTimeZone
        mSelectedDay?.normalize(true)
        mToday!!.timezone = mHomeTimeZone
        mToday?.setToNow()
        mTempTime?.switchTimezone(mHomeTimeZone)
    }

    @Override
    override fun setSelectedDay(selectedTime: Time?) {
        mSelectedDay?.set(selectedTime)
        val millis: Long = mSelectedDay!!.normalize(true)
        mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay(
            Time.getJulianDay(millis, mSelectedDay!!.gmtoff), mFirstDayOfWeek
        )
        notifyDataSetChanged()
    }

    fun setEvents(firstJulianDay: Int, numDays: Int, events: ArrayList<Event>?) {
        if (mIsMiniMonth) {
            if (Log.isLoggable(TAG, Log.ERROR)) {
                Log.e(
                    TAG, "Attempted to set events for mini view. Events only supported in full" +
                        " view."
                )
            }
            return
        }
        mEvents = events
        mFirstJulianDay = firstJulianDay
        mQueryDays = numDays
        // Create a new list, this is necessary since the weeks are referencing
        // pieces of the old list
        val eventDayList: ArrayList<ArrayList<Event>> = ArrayList<ArrayList<Event>>()
        for (i in 0 until numDays) {
            eventDayList.add(ArrayList<Event>())
        }
        if (events == null || events.size == 0) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "No events. Returning early--go schedule something fun.")
            }
            mEventDayList = eventDayList
            refresh()
            return
        }

        // Compute the new set of days with events
        for (event in events) {
            var startDay: Int = event.startDay - mFirstJulianDay
            var endDay: Int = event.endDay - mFirstJulianDay + 1
            if (startDay < numDays || endDay >= 0) {
                if (startDay < 0) {
                    startDay = 0
                }
                if (startDay > numDays) {
                    continue
                }
                if (endDay < 0) {
                    continue
                }
                if (endDay > numDays) {
                    endDay = numDays
                }
                for (j in startDay until endDay) {
                    eventDayList.get(j).add(event)
                }
            }
        }
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Processed " + events.size.toString() + " events.")
        }
        mEventDayList = eventDayList
        refresh()
    }

    @SuppressWarnings("unchecked")
    @Override
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        if (mIsMiniMonth) {
            return super.getView(position, convertView, parent)
        }
        var v: MonthWeekEventsView
        val params = LayoutParams(
            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
        )
        var drawingParams: HashMap<String?, Int?>? = null
        var isAnimatingToday = false
        if (convertView != null) {
            v = convertView as MonthWeekEventsView
            // Checking updateToday uses the current params instead of the new
            // params, so this is assuming the view is relatively stable
            if (mAnimateToday && v.updateToday(mSelectedDay!!.timezone)) {
                val currentTime: Long = System.currentTimeMillis()
                // If it's been too long since we tried to start the animation
                // don't show it. This can happen if the user stops a scroll
                // before reaching today.
                if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) {
                    mAnimateToday = false
                    mAnimateTime = 0
                } else {
                    isAnimatingToday = true
                    // There is a bug that causes invalidates to not work some
                    // of the time unless we recreate the view.
                    v = MonthWeekEventsView(mContext)
                }
            } else {
                drawingParams = v.getTag() as HashMap<String?, Int?>
            }
        } else {
            v = MonthWeekEventsView(mContext)
        }
        if (drawingParams == null) {
            drawingParams = HashMap<String?, Int?>()
        }
        drawingParams.clear()
        v.setLayoutParams(params)
        v.setClickable(true)
        v.setOnTouchListener(this)
        var selectedDay = -1
        if (mSelectedWeek === position) {
            selectedDay = mSelectedDay!!.weekDay
        }
        drawingParams.put(
            SimpleWeekView.VIEW_PARAMS_HEIGHT,
            (parent.getHeight() + parent.getTop()) / mNumWeeks
        )
        drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay)
        drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, if (mShowWeekNumber) 1 else 0)
        drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek)
        drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek)
        drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position)
        drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth)
        drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation)
        if (isAnimatingToday) {
            drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1)
            mAnimateToday = false
        }
        v.setWeekParams(drawingParams, mSelectedDay!!.timezone)
        return v
    }

    @Override
    internal override fun refresh() {
        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext)
        mShowWeekNumber = Utils.getShowWeekNumber(mContext)
        mHomeTimeZone = Utils.getTimeZone(mContext, null)
        mOrientation = mContext.getResources().getConfiguration().orientation
        updateTimeZones()
        notifyDataSetChanged()
    }

    @Override
    protected override fun onDayTapped(day: Time) {
        setDayParameters(day)
        if (mShowAgendaWithMonth || mIsMiniMonth) {
            // If agenda view is visible with month view , refresh the views
            // with the selected day's info
            mController?.sendEvent(
                mContext as Object?, EventType.GO_TO, day, day, -1,
                ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null
            )
        } else {
            // Else , switch to the detailed view
            mController?.sendEvent(
                mContext as Object?, EventType.GO_TO, day, day, -1,
                ViewType.DETAIL, CalendarController.EXTRA_GOTO_DATE
                    or CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null
            )
        }
    }

    private fun setDayParameters(day: Time) {
        day.timezone = mHomeTimeZone
        val currTime = Time(mHomeTimeZone)
        currTime.set(mController!!.time as Long)
        day.hour = currTime.hour
        day.minute = currTime.minute
        day.allDay = false
        day.normalize(true)
    }

    @Override
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        if (v !is MonthWeekEventsView) {
            return super.onTouch(v, event)
        }
        val action: Int = event!!.getAction()

        // Event was tapped - switch to the detailed view making sure the click animation
        // is done first.
        if (mGestureDetector!!.onTouchEvent(event)) {
            mSingleTapUpView = v as MonthWeekEventsView?
            val delay: Long = System.currentTimeMillis() - mClickTime
            // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms
            mListView?.postDelayed(
                mDoSingleTapUp,
                if (delay > mTotalClickDelay) 0 else mTotalClickDelay - delay
            )
            return true
        } else {
            // Animate a click - on down: show the selected day in the "clicked" color.
            // On Up/scroll/move/cancel: hide the "clicked" color.
            when (action) {
                MotionEvent.ACTION_DOWN -> {
                    mClickedView = v as MonthWeekEventsView
                    mClickedXLocation = event.getX()
                    mClickTime = System.currentTimeMillis()
                    mListView?.postDelayed(mDoClick, mOnDownDelay.toLong())
                }
                MotionEvent.ACTION_UP, MotionEvent.ACTION_SCROLL, MotionEvent.ACTION_CANCEL ->
                    clearClickedView(
                    v as MonthWeekEventsView?
                )
                MotionEvent.ACTION_MOVE -> // No need to cancel on vertical movement,
                    // ACTION_SCROLL will do that.
                    if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) {
                        clearClickedView(v as MonthWeekEventsView?)
                    }
                else -> {
                }
            }
        }
        // Do not tell the frameworks we consumed the touch action so that fling actions can be
        // processed by the fragment.
        return false
    }

    /**
     * This is here so we can identify events and process them
     */
    protected inner class CalendarGestureListener : GestureDetector.SimpleOnGestureListener() {
        @Override
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            return true
        }

        @Override
        override fun onLongPress(e: MotionEvent) {
            if (mLongClickedView != null) {
                val day: Time? = mLongClickedView?.getDayFromLocation(mClickedXLocation)
                if (day != null) {
                    mLongClickedView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
                    val message = Message()
                    message.obj = day
                }
                mLongClickedView?.clearClickedDay()
                mLongClickedView = null
            }
        }
    }

    // Clear the visual cues of the click animation and related running code.
    private fun clearClickedView(v: MonthWeekEventsView?) {
        mListView?.removeCallbacks(mDoClick)
        synchronized(v as Any) { v?.clearClickedDay() }
        mClickedView = null
    }

    // Perform the tap animation in a runnable to allow a delay before showing the tap color.
    // This is done to prevent a click animation when a fling is done.
    private val mDoClick: Runnable = object : Runnable {
        @Override
        override fun run() {
            if (mClickedView != null) {
                synchronized(mClickedView as MonthWeekEventsView) {
                    mClickedView?.setClickedDay(mClickedXLocation) }
                mLongClickedView = mClickedView
                mClickedView = null
                // This is a workaround , sometimes the top item on the listview doesn't refresh on
                // invalidate, so this forces a re-draw.
                mListView?.invalidate()
            }
        }
    }

    // Performs the single tap operation: go to the tapped day.
    // This is done in a runnable to allow the click animation to finish before switching views
    private val mDoSingleTapUp: Runnable = object : Runnable {
        @Override
        override fun run() {
            if (mSingleTapUpView != null) {
                val day: Time? = mSingleTapUpView?.getDayFromLocation(mClickedXLocation)
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(
                        TAG,
                        "Touched day at Row=" + mSingleTapUpView?.mWeek?.toString() +
                            " day=" + day?.toString()
                    )
                }
                if (day != null) {
                    onDayTapped(day)
                }
                clearClickedView(mSingleTapUpView)
                mSingleTapUpView = null
            }
        }
    }

    companion object {
        private const val TAG = "MonthByWeekAdapter"
        const val WEEK_PARAMS_IS_MINI = "mini_month"
        protected var DEFAULT_QUERY_DAYS = 7 * 8 // 8 weeks
        private const val ANIMATE_TODAY_TIMEOUT: Long = 1000

        // Used to insure minimal time for seeing the click animation before switching views
        private const val mOnTapDelay = 100

        // Minimal time for a down touch action before stating the click animation, this ensures
        // that there is no click animation on flings
        private var mOnDownDelay: Int = 0
        private var mTotalClickDelay: Int = 0

        // Minimal distance to move the finger in order to cancel the click animation
        private var mMovedPixelToCancel: Float = 0f
    }

    init {
        if (params.containsKey(WEEK_PARAMS_IS_MINI)) {
            mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0
        }
        mShowAgendaWithMonth = Utils.getConfigBool(context as Context,
            R.bool.show_agenda_with_month)
        val vc: ViewConfiguration = ViewConfiguration.get(context)
        mOnDownDelay = ViewConfiguration.getTapTimeout()
        mMovedPixelToCancel = vc.getScaledTouchSlop().toFloat()
        mTotalClickDelay = mOnDownDelay + mOnTapDelay
    }
}