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 }