blob: da617950371a8772e96cf72543c0a045f936c705 [file] [log] [blame]
/*
* 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
}
}