| /* |
| * 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 |
| } |
| } |