/* * 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) : 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>() protected var mEvents: ArrayList? = 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?) { 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>() for (i in 0 until numDays) { eventDayList.add(ArrayList()) } 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? = 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 } } else { v = MonthWeekEventsView(mContext) } if (drawingParams == null) { drawingParams = HashMap() } 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 } }