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 com.android.calendar.Event
19 import com.android.calendar.R
20 import com.android.calendar.Utils
21 import android.animation.Animator
22 import android.animation.AnimatorListenerAdapter
23 import android.animation.ObjectAnimator
24 import android.app.Service
25 import android.content.Context
26 import android.content.res.Configuration
27 import android.content.res.Resources
28 import android.graphics.Canvas
29 import android.graphics.Color
30 import android.graphics.Paint
31 import android.graphics.Paint.Align
32 import android.graphics.Paint.Style
33 import android.graphics.Typeface
34 import android.graphics.drawable.Drawable
35 import android.provider.CalendarContract.Attendees
36 import android.text.TextPaint
37 import android.text.TextUtils
38 import android.text.format.DateFormat
39 import android.text.format.DateUtils
40 import android.text.format.Time
41 import android.util.Log
42 import android.view.MotionEvent
43 import android.view.accessibility.AccessibilityEvent
44 import android.view.accessibility.AccessibilityManager
45 import java.util.ArrayList
46 import java.util.Arrays
47 import java.util.Formatter
48 import java.util.HashMap
49 import java.util.Iterator
50 import java.util.List
51 import java.util.Locale
52 
53 class MonthWeekEventsView
54 /**
55  * Shows up as an error if we don't include this.
56  */
57 (context: Context) : SimpleWeekView(context) {
58     // Renamed to avoid override modifier and type mismatch error
59     protected val mTodayTime: Time = Time()
60     override protected var mHasToday = false
61     protected var mTodayIndex = -1
62     protected var mOrientation: Int = Configuration.ORIENTATION_LANDSCAPE
63     protected var mEvents: List<ArrayList<Event?>>? = null
64     protected var mUnsortedEvents: ArrayList<Event?>? = null
65     var mDna: HashMap<Int, Utils.DNAStrand>? = null
66 
67     // This is for drawing the outlines around event chips and supports up to 10
68     // events being drawn on each day. The code will expand this if necessary.
69     protected var mEventOutlines: FloatRef = FloatRef(10 * 4 * 4 * 7)
70     protected var mMonthNamePaint: Paint? = null
71     protected var mEventPaint: TextPaint = TextPaint()
72     protected var mSolidBackgroundEventPaint: TextPaint? = null
73     protected var mFramedEventPaint: TextPaint? = null
74     protected var mDeclinedEventPaint: TextPaint? = null
75     protected var mEventExtrasPaint: TextPaint = TextPaint()
76     protected var mEventDeclinedExtrasPaint: TextPaint = TextPaint()
77     protected var mWeekNumPaint: Paint = Paint()
78     protected var mDNAAllDayPaint: Paint = Paint()
79     protected var mDNATimePaint: Paint = Paint()
80     protected var mEventSquarePaint: Paint = Paint()
81     protected var mTodayDrawable: Drawable? = null
82     protected var mMonthNumHeight = 0
83     protected var mMonthNumAscentHeight = 0
84     protected var mEventHeight = 0
85     protected var mEventAscentHeight = 0
86     protected var mExtrasHeight = 0
87     protected var mExtrasAscentHeight = 0
88     protected var mExtrasDescent = 0
89     protected var mWeekNumAscentHeight = 0
90     protected var mMonthBGColor = 0
91     protected var mMonthBGOtherColor = 0
92     protected var mMonthBGTodayColor = 0
93     protected var mMonthNumColor = 0
94     protected var mMonthNumOtherColor = 0
95     protected var mMonthNumTodayColor = 0
96     protected var mMonthNameColor = 0
97     protected var mMonthNameOtherColor = 0
98     protected var mMonthEventColor = 0
99     protected var mMonthDeclinedEventColor = 0
100     protected var mMonthDeclinedExtrasColor = 0
101     protected var mMonthEventExtraColor = 0
102     protected var mMonthEventOtherColor = 0
103     protected var mMonthEventExtraOtherColor = 0
104     protected var mMonthWeekNumColor = 0
105     protected var mMonthBusyBitsBgColor = 0
106     protected var mMonthBusyBitsBusyTimeColor = 0
107     protected var mMonthBusyBitsConflictTimeColor = 0
108     private var mClickedDayIndex = -1
109     private var mClickedDayColor = 0
110     protected var mEventChipOutlineColor = -0x1
111     protected var mDaySeparatorInnerColor = 0
112     protected var mTodayAnimateColor = 0
113     private var mAnimateToday = false
114     private var mAnimateTodayAlpha = 0
115     private var mTodayAnimator: ObjectAnimator? = null
116     private val mAnimatorListener: TodayAnimatorListener = TodayAnimatorListener()
117 
118     internal inner class TodayAnimatorListener : AnimatorListenerAdapter() {
119         @Volatile
120         private var mAnimator: Animator? = null
121 
122         @Volatile
123         private var mFadingIn = false
124         @Override
onAnimationEndnull125         override fun onAnimationEnd(animation: Animator) {
126             synchronized(this) {
127                 if (mAnimator !== animation) {
128                     animation.removeAllListeners()
129                     animation.cancel()
130                     return
131                 }
132                 if (mFadingIn) {
133                     if (mTodayAnimator != null) {
134                         mTodayAnimator?.removeAllListeners()
135                         mTodayAnimator?.cancel()
136                     }
137                     mTodayAnimator = ObjectAnimator.ofInt(this@MonthWeekEventsView,
138                             "animateTodayAlpha", 255, 0)
139                     mAnimator = mTodayAnimator
140                     mFadingIn = false
141                     mTodayAnimator?.addListener(this)
142                     mTodayAnimator?.setDuration(600)
143                     mTodayAnimator?.start()
144                 } else {
145                     mAnimateToday = false
146                     mAnimateTodayAlpha = 0
147                     mAnimator?.removeAllListeners()
148                     mAnimator = null
149                     mTodayAnimator = null
150                     invalidate()
151                 }
152             }
153         }
154 
setAnimatornull155         fun setAnimator(animation: Animator?) {
156             mAnimator = animation
157         }
158 
setFadingInnull159         fun setFadingIn(fadingIn: Boolean) {
160             mFadingIn = fadingIn
161         }
162     }
163 
164     private var mDayXs: IntArray? = null
165 
166     /**
167      * This provides a reference to a float array which allows for easy size
168      * checking and reallocation. Used for drawing lines.
169      */
170     inner class FloatRef(size: Int) {
171         var array: FloatArray
ensureSizenull172         fun ensureSize(newSize: Int) {
173             if (newSize >= array.size) {
174                 // Add enough space for 7 more boxes to be drawn
175                 array = Arrays.copyOf(array, newSize + 16 * 7)
176             }
177         }
178 
179         init {
180             array = FloatArray(size)
181         }
182     }
183 
184     // Sets the list of events for this week. Takes a sorted list of arrays
185     // divided up by day for generating the large month version and the full
186     // arraylist sorted by start time to generate the dna version.
setEventsnull187     fun setEvents(sortedEvents: List<ArrayList<Event?>>?, unsortedEvents: ArrayList<Event?>?) {
188         setEvents(sortedEvents)
189         // The MIN_WEEK_WIDTH is a hack to prevent the view from trying to
190         // generate dna bits before its width has been fixed.
191         createDna(unsortedEvents)
192     }
193 
194     /**
195      * Sets up the dna bits for the view. This will return early if the view
196      * isn't in a state that will create a valid set of dna yet (such as the
197      * views width not being set correctly yet).
198      */
createDnanull199     fun createDna(unsortedEvents: ArrayList<Event?>?) {
200         if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) {
201             // Stash the list of events for use when this view is ready, or
202             // just clear it if a null set has been passed to this view
203             mUnsortedEvents = unsortedEvents
204             mDna = null
205             return
206         } else {
207             // clear the cached set of events since we're ready to build it now
208             mUnsortedEvents = null
209         }
210         // Create the drawing coordinates for dna
211         if (!mShowDetailsInMonth) {
212             val numDays: Int = mEvents!!.size
213             var effectiveWidth: Int = mWidth - mPadding * 2
214             if (mShowWeekNum) {
215                 effectiveWidth -= SPACING_WEEK_NUMBER
216             }
217             DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING
218             mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH.toFloat())
219             mDayXs = IntArray(numDays)
220             for (day in 0 until numDays) {
221                 mDayXs!![day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING
222             }
223             val top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1
224             val bottom: Int = mHeight - DNA_MARGIN
225             mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom,
226                     DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext())
227         }
228     }
229 
setEventsnull230     fun setEvents(sortedEvents: List<ArrayList<Event?>>?) {
231         mEvents = sortedEvents
232         if (sortedEvents == null) {
233             return
234         }
235         if (sortedEvents.size !== mNumDays) {
236             if (Log.isLoggable(TAG, Log.ERROR)) {
237                 Log.wtf(TAG, ("Events size must be same as days displayed: size="
238                         + sortedEvents.size) + " days=" + mNumDays)
239             }
240             mEvents = null
241             return
242         }
243     }
244 
loadColorsnull245     protected fun loadColors(context: Context) {
246         val res: Resources = context.getResources()
247         mMonthWeekNumColor = res.getColor(R.color.month_week_num_color)
248         mMonthNumColor = res.getColor(R.color.month_day_number)
249         mMonthNumOtherColor = res.getColor(R.color.month_day_number_other)
250         mMonthNumTodayColor = res.getColor(R.color.month_today_number)
251         mMonthNameColor = mMonthNumColor
252         mMonthNameOtherColor = mMonthNumOtherColor
253         mMonthEventColor = res.getColor(R.color.month_event_color)
254         mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color)
255         mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color)
256         mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color)
257         mMonthEventOtherColor = res.getColor(R.color.month_event_other_color)
258         mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color)
259         mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor)
260         mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor)
261         mMonthBGColor = res.getColor(R.color.month_bgcolor)
262         mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines)
263         mTodayAnimateColor = res.getColor(R.color.today_highlight_color)
264         mClickedDayColor = res.getColor(R.color.day_clicked_background_color)
265         mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light)
266     }
267 
268     /**
269      * Sets up the text and style properties for painting. Override this if you
270      * want to use a different paint.
271      */
272     @Override
initViewnull273     protected override fun initView() {
274         super.initView()
275         if (!mInitialized) {
276             val resources: Resources = getContext().getResources()
277             mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month)
278             TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title)
279             TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number)
280             SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin)
281             CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color)
282             EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color)
283             if (mScale != 1f) {
284                 TOP_PADDING_MONTH_NUMBER *= mScale.toInt()
285                 TOP_PADDING_WEEK_NUMBER *= mScale.toInt()
286                 SIDE_PADDING_MONTH_NUMBER *= mScale.toInt()
287                 SIDE_PADDING_WEEK_NUMBER *= mScale.toInt()
288                 SPACING_WEEK_NUMBER *= mScale.toInt()
289                 TEXT_SIZE_MONTH_NUMBER *= mScale.toInt()
290                 TEXT_SIZE_EVENT *= mScale.toInt()
291                 TEXT_SIZE_EVENT_TITLE *= mScale.toInt()
292                 TEXT_SIZE_MORE_EVENTS *= mScale.toInt()
293                 TEXT_SIZE_MONTH_NAME *= mScale.toInt()
294                 TEXT_SIZE_WEEK_NUM *= mScale.toInt()
295                 DAY_SEPARATOR_OUTER_WIDTH *= mScale.toInt()
296                 DAY_SEPARATOR_INNER_WIDTH *= mScale.toInt()
297                 DAY_SEPARATOR_VERTICAL_LENGTH *= mScale.toInt()
298                 DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT *= mScale.toInt()
299                 EVENT_X_OFFSET_LANDSCAPE *= mScale.toInt()
300                 EVENT_Y_OFFSET_LANDSCAPE *= mScale.toInt()
301                 EVENT_Y_OFFSET_PORTRAIT *= mScale.toInt()
302                 EVENT_SQUARE_WIDTH *= mScale.toInt()
303                 EVENT_SQUARE_BORDER *= mScale.toInt()
304                 EVENT_LINE_PADDING *= mScale.toInt()
305                 EVENT_BOTTOM_PADDING *= mScale.toInt()
306                 EVENT_RIGHT_PADDING *= mScale.toInt()
307                 DNA_MARGIN *= mScale.toInt()
308                 DNA_WIDTH *= mScale.toInt()
309                 DNA_ALL_DAY_HEIGHT *= mScale.toInt()
310                 DNA_MIN_SEGMENT_HEIGHT *= mScale.toInt()
311                 DNA_SIDE_PADDING *= mScale.toInt()
312                 DEFAULT_EDGE_SPACING *= mScale.toInt()
313                 DNA_ALL_DAY_WIDTH *= mScale.toInt()
314                 TODAY_HIGHLIGHT_WIDTH *= mScale.toInt()
315             }
316             if (!mShowDetailsInMonth) {
317                 TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN
318             }
319             mInitialized = true
320         }
321         mPadding = DEFAULT_EDGE_SPACING
322         loadColors(getContext())
323         // TODO modify paint properties depending on isMini
324         mMonthNumPaint = Paint()
325         mMonthNumPaint.setFakeBoldText(false)
326         mMonthNumPaint.setAntiAlias(true)
327         mMonthNumPaint.setTextSize(TEXT_SIZE_MONTH_NUMBER.toFloat())
328         mMonthNumPaint.setColor(mMonthNumColor)
329         mMonthNumPaint.setStyle(Style.FILL)
330         mMonthNumPaint.setTextAlign(Align.RIGHT)
331         mMonthNumPaint.setTypeface(Typeface.DEFAULT)
332         mMonthNumAscentHeight = (-mMonthNumPaint.ascent() + 0.5f).toInt()
333         mMonthNumHeight = (mMonthNumPaint.descent() - mMonthNumPaint.ascent() + 0.5f).toInt()
334         mEventPaint = TextPaint()
335         mEventPaint.setFakeBoldText(true)
336         mEventPaint.setAntiAlias(true)
337         mEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE.toFloat())
338         mEventPaint.setColor(mMonthEventColor)
339         mSolidBackgroundEventPaint = TextPaint(mEventPaint)
340         mSolidBackgroundEventPaint?.setColor(EVENT_TEXT_COLOR)
341         mFramedEventPaint = TextPaint(mSolidBackgroundEventPaint)
342         mDeclinedEventPaint = TextPaint()
343         mDeclinedEventPaint?.setFakeBoldText(true)
344         mDeclinedEventPaint?.setAntiAlias(true)
345         mDeclinedEventPaint?.setTextSize(TEXT_SIZE_EVENT_TITLE.toFloat())
346         mDeclinedEventPaint?.setColor(mMonthDeclinedEventColor)
347         mEventAscentHeight = (-mEventPaint.ascent() + 0.5f).toInt()
348         mEventHeight = (mEventPaint.descent() - mEventPaint.ascent() + 0.5f).toInt()
349         mEventExtrasPaint = TextPaint()
350         mEventExtrasPaint.setFakeBoldText(false)
351         mEventExtrasPaint.setAntiAlias(true)
352         mEventExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat())
353         mEventExtrasPaint.setTextSize(TEXT_SIZE_EVENT.toFloat())
354         mEventExtrasPaint.setColor(mMonthEventExtraColor)
355         mEventExtrasPaint.setStyle(Style.FILL)
356         mEventExtrasPaint.setTextAlign(Align.LEFT)
357         mExtrasHeight = (mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f).toInt()
358         mExtrasAscentHeight = (-mEventExtrasPaint.ascent() + 0.5f).toInt()
359         mExtrasDescent = (mEventExtrasPaint.descent() + 0.5f).toInt()
360         mEventDeclinedExtrasPaint = TextPaint()
361         mEventDeclinedExtrasPaint.setFakeBoldText(false)
362         mEventDeclinedExtrasPaint.setAntiAlias(true)
363         mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat())
364         mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT.toFloat())
365         mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor)
366         mEventDeclinedExtrasPaint.setStyle(Style.FILL)
367         mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT)
368         mWeekNumPaint = Paint()
369         mWeekNumPaint.setFakeBoldText(false)
370         mWeekNumPaint.setAntiAlias(true)
371         mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM.toFloat())
372         mWeekNumPaint.setColor(mWeekNumColor)
373         mWeekNumPaint.setStyle(Style.FILL)
374         mWeekNumPaint.setTextAlign(Align.RIGHT)
375         mWeekNumAscentHeight = (-mWeekNumPaint.ascent() + 0.5f).toInt()
376         mDNAAllDayPaint = Paint()
377         mDNATimePaint = Paint()
378         mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor)
379         mDNATimePaint.setStyle(Style.FILL_AND_STROKE)
380         mDNATimePaint.setStrokeWidth(DNA_WIDTH.toFloat())
381         mDNATimePaint.setAntiAlias(false)
382         mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor)
383         mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE)
384         mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH.toFloat())
385         mDNAAllDayPaint.setAntiAlias(false)
386         mEventSquarePaint = Paint()
387         mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat())
388         mEventSquarePaint.setAntiAlias(false)
389         if (DEBUG_LAYOUT) {
390             Log.d("EXTRA", "mScale=$mScale")
391             Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint.ascent()
392                     .toString() + " descent=" + mMonthNumPaint.descent().toString() +
393                     " int height=" + mMonthNumHeight)
394             Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint.ascent()
395                     .toString() + " descent=" + mEventPaint.descent().toString() +
396                     " int height=" + mEventHeight
397                     .toString() + " int ascent=" + mEventAscentHeight)
398             Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent()
399                     .toString() + " descent=" + mEventExtrasPaint.descent().toString() +
400                     " int height=" + mExtrasHeight)
401             Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent()
402                     .toString() + " descent=" + mWeekNumPaint.descent())
403         }
404     }
405 
406     @Override
setWeekParamsnull407     override fun setWeekParams(params: HashMap<String?, Int?>, tz: String) {
408         super.setWeekParams(params, tz)
409         if (params.containsKey(VIEW_PARAMS_ORIENTATION)) {
410             mOrientation = params.get(VIEW_PARAMS_ORIENTATION) ?:
411                     Configuration.ORIENTATION_LANDSCAPE
412         }
413         updateToday(tz)
414         mNumCells = mNumDays + 1
415         if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) {
416             synchronized(mAnimatorListener) {
417                 if (mTodayAnimator != null) {
418                     mTodayAnimator?.removeAllListeners()
419                     mTodayAnimator?.cancel()
420                 }
421                 mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha",
422                         Math.max(mAnimateTodayAlpha, 80), 255)
423                 mTodayAnimator?.setDuration(150)
424                 mAnimatorListener.setAnimator(mTodayAnimator)
425                 mAnimatorListener.setFadingIn(true)
426                 mTodayAnimator?.addListener(mAnimatorListener)
427                 mAnimateToday = true
428                 mTodayAnimator?.start()
429             }
430         }
431     }
432 
433     /**
434      * @param tz
435      */
updateTodaynull436     fun updateToday(tz: String): Boolean {
437         mTodayTime.timezone = tz
438         mTodayTime.setToNow()
439         mTodayTime.normalize(true)
440         val julianToday: Int = Time.getJulianDay(mTodayTime.toMillis(false), mTodayTime.gmtoff)
441         if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) {
442             mHasToday = true
443             mTodayIndex = julianToday - mFirstJulianDay
444         } else {
445             mHasToday = false
446             mTodayIndex = -1
447         }
448         return mHasToday
449     }
450 
setAnimateTodayAlphanull451     fun setAnimateTodayAlpha(alpha: Int) {
452         mAnimateTodayAlpha = alpha
453         invalidate()
454     }
455 
456     @Override
onDrawnull457     protected override fun onDraw(canvas: Canvas) {
458         drawBackground(canvas)
459         drawWeekNums(canvas)
460         drawDaySeparators(canvas)
461         if (mHasToday && mAnimateToday) {
462             drawToday(canvas)
463         }
464         if (mShowDetailsInMonth) {
465             drawEvents(canvas)
466         } else {
467             if (mDna == null && mUnsortedEvents != null) {
468                 createDna(mUnsortedEvents)
469             }
470             drawDNA(canvas)
471         }
472         drawClick(canvas)
473     }
474 
drawTodaynull475     protected fun drawToday(canvas: Canvas) {
476         r.top = DAY_SEPARATOR_INNER_WIDTH + TODAY_HIGHLIGHT_WIDTH / 2
477         r.bottom = mHeight - Math.ceil(TODAY_HIGHLIGHT_WIDTH.toDouble() / 2.0f).toInt()
478         p.setStyle(Style.STROKE)
479         p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH.toFloat())
480         r.left = computeDayLeftPosition(mTodayIndex) + TODAY_HIGHLIGHT_WIDTH / 2
481         r.right = (computeDayLeftPosition(mTodayIndex + 1)
482                 - Math.ceil(TODAY_HIGHLIGHT_WIDTH.toDouble() / 2.0f).toInt())
483         p.setColor(mTodayAnimateColor or (mAnimateTodayAlpha shl 24))
484         canvas.drawRect(r, p)
485         p.setStyle(Style.FILL)
486     }
487 
488     // TODO move into SimpleWeekView
489     // Computes the x position for the left side of the given day
computeDayLeftPositionnull490     private fun computeDayLeftPosition(day: Int): Int {
491         var effectiveWidth: Int = mWidth
492         var x = 0
493         var xOffset = 0
494         if (mShowWeekNum) {
495             xOffset = SPACING_WEEK_NUMBER + mPadding
496             effectiveWidth -= xOffset
497         }
498         x = day * effectiveWidth / mNumDays + xOffset
499         return x
500     }
501 
502     @Override
drawDaySeparatorsnull503     protected override fun drawDaySeparators(canvas: Canvas) {
504         val lines = FloatArray(8 * 4)
505         var count = 6 * 4
506         var wkNumOffset = 0
507         var i = 0
508         if (mShowWeekNum) {
509             // This adds the first line separating the week number
510             val xOffset: Int = SPACING_WEEK_NUMBER + mPadding
511             count += 4
512             lines[i++] = xOffset.toFloat()
513             lines[i++] = 0f
514             lines[i++] = xOffset.toFloat()
515             lines[i++] = mHeight.toFloat()
516             wkNumOffset++
517         }
518         count += 4
519         lines[i++] = 0f
520         lines[i++] = 0f
521         lines[i++] = mWidth.toFloat()
522         lines[i++] = 0f
523         val y0 = 0
524         val y1: Int = mHeight
525         while (i < count) {
526             val x = computeDayLeftPosition(i / 4 - wkNumOffset)
527             lines[i++] = x.toFloat()
528             lines[i++] = y0.toFloat()
529             lines[i++] = x.toFloat()
530             lines[i++] = y1.toFloat()
531         }
532         p.setColor(mDaySeparatorInnerColor)
533         p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH.toFloat())
534         canvas.drawLines(lines, 0, count, p)
535     }
536 
537     @Override
drawBackgroundnull538     protected override fun drawBackground(canvas: Canvas) {
539         var i = 0
540         var offset = 0
541         r.top = DAY_SEPARATOR_INNER_WIDTH
542         r.bottom = mHeight
543         if (mShowWeekNum) {
544             i++
545             offset++
546         }
547         if (!mOddMonth.get(i)) {
548             while (++i < mOddMonth.size && !mOddMonth.get(i));
549             r.right = computeDayLeftPosition(i - offset)
550             r.left = 0
551             p.setColor(mMonthBGOtherColor)
552             canvas.drawRect(r, p)
553             // compute left edge for i, set up r, draw
554         } else if (!mOddMonth.get(mOddMonth.size - 1.also { i = it })) {
555             while (--i >= offset && !mOddMonth.get(i));
556             i++
557             // compute left edge for i, set up r, draw
558             r.right = mWidth
559             r.left = computeDayLeftPosition(i - offset)
560             p.setColor(mMonthBGOtherColor)
561             canvas.drawRect(r, p)
562         }
563         if (mHasToday) {
564             p.setColor(mMonthBGTodayColor)
565             r.left = computeDayLeftPosition(mTodayIndex)
566             r.right = computeDayLeftPosition(mTodayIndex + 1)
567             canvas.drawRect(r, p)
568         }
569     }
570 
571     // Draw the "clicked" color on the tapped day
drawClicknull572     private fun drawClick(canvas: Canvas) {
573         if (mClickedDayIndex != -1) {
574             val alpha: Int = p.getAlpha()
575             p.setColor(mClickedDayColor)
576             p.setAlpha(mClickedAlpha)
577             r.left = computeDayLeftPosition(mClickedDayIndex)
578             r.right = computeDayLeftPosition(mClickedDayIndex + 1)
579             r.top = DAY_SEPARATOR_INNER_WIDTH
580             r.bottom = mHeight
581             canvas.drawRect(r, p)
582             p.setAlpha(alpha)
583         }
584     }
585 
586     @Override
drawWeekNumsnull587     protected override fun drawWeekNums(canvas: Canvas) {
588         var y: Int
589         var i = 0
590         var offset = -1
591         var todayIndex = mTodayIndex
592         var x = 0
593         var numCount: Int = mNumDays
594         if (mShowWeekNum) {
595             x = SIDE_PADDING_WEEK_NUMBER + mPadding
596             y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER
597             canvas.drawText(mDayNumbers!!.get(0) as String, x.toFloat(), y.toFloat(), mWeekNumPaint)
598             numCount++
599             i++
600             todayIndex++
601             offset++
602         }
603         y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER
604         var isFocusMonth: Boolean = mFocusDay.get(i)
605         var isBold = false
606         mMonthNumPaint.setColor(if (isFocusMonth) mMonthNumColor else mMonthNumOtherColor)
607         while (i < numCount) {
608             if (mHasToday && todayIndex == i) {
609                 mMonthNumPaint.setColor(mMonthNumTodayColor)
610                 mMonthNumPaint.setFakeBoldText(true.also { isBold = it })
611                 if (i + 1 < numCount) {
612                     // Make sure the color will be set back on the next
613                     // iteration
614                     isFocusMonth = !mFocusDay.get(i + 1)
615                 }
616             } else if (mFocusDay.get(i) !== isFocusMonth) {
617                 isFocusMonth = mFocusDay.get(i)
618                 mMonthNumPaint.setColor(if (isFocusMonth) mMonthNumColor else mMonthNumOtherColor)
619             }
620             x = computeDayLeftPosition(i - offset) - SIDE_PADDING_MONTH_NUMBER
621             canvas.drawText(mDayNumbers!!.get(i) as String, x.toFloat(), y.toFloat(),
622                     mMonthNumPaint as Paint)
623             if (isBold) {
624                 mMonthNumPaint.setFakeBoldText(false.also { isBold = it })
625             }
626             i++
627         }
628     }
629 
drawEventsnull630     protected fun drawEvents(canvas: Canvas) {
631         if (mEvents == null) {
632             return
633         }
634         var day = -1
635         for (eventDay in mEvents!!) {
636             day++
637             if (eventDay == null || eventDay.size === 0) {
638                 continue
639             }
640             var ySquare: Int
641             val xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1
642             var rightEdge = computeDayLeftPosition(day + 1)
643             if (mOrientation == Configuration.ORIENTATION_PORTRAIT) {
644                 ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER
645                 rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1
646             } else {
647                 ySquare = EVENT_Y_OFFSET_LANDSCAPE
648                 rightEdge -= EVENT_X_OFFSET_LANDSCAPE
649             }
650 
651             // Determine if everything will fit when time ranges are shown.
652             var showTimes = true
653             var iter: Iterator<Event> = eventDay.iterator() as Iterator<Event>
654             var yTest = ySquare
655             while (iter.hasNext()) {
656                 val event: Event = iter.next()
657                 val newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(),
658                         showTimes,  /*doDraw*/false)
659                 if (newY == yTest) {
660                     showTimes = false
661                     break
662                 }
663                 yTest = newY
664             }
665             var eventCount = 0
666             iter = eventDay.iterator() as Iterator<Event>
667             while (iter.hasNext()) {
668                 val event: Event = iter.next()
669                 val newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(),
670                         showTimes,  /*doDraw*/true)
671                 if (newY == ySquare) {
672                     break
673                 }
674                 eventCount++
675                 ySquare = newY
676             }
677             val remaining: Int = eventDay.size- eventCount
678             if (remaining > 0) {
679                 drawMoreEvents(canvas, remaining, xSquare)
680             }
681         }
682     }
683 
addChipOutlinenull684     protected fun addChipOutline(lines: FloatRef, count: Int, x: Int, y: Int): Int {
685         var count = count
686         lines.ensureSize(count + 16)
687         // top of box
688         lines.array[count++] = x.toFloat()
689         lines.array[count++] = y.toFloat()
690         lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat()
691         lines.array[count++] = y.toFloat()
692         // right side of box
693         lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat()
694         lines.array[count++] = y.toFloat()
695         lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat()
696         lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat()
697         // left side of box
698         lines.array[count++] = x.toFloat()
699         lines.array[count++] = y.toFloat()
700         lines.array[count++] = x.toFloat()
701         lines.array[count++] = (y + EVENT_SQUARE_WIDTH + 1).toFloat()
702         // bottom of box
703         lines.array[count++] = x.toFloat()
704         lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat()
705         lines.array[count++] = (x + EVENT_SQUARE_WIDTH + 1).toFloat()
706         lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat()
707         return count
708     }
709 
710     /**
711      * Attempts to draw the given event. Returns the y for the next event or the
712      * original y if the event will not fit. An event is considered to not fit
713      * if the event and its extras won't fit or if there are more events and the
714      * more events line would not fit after drawing this event.
715      *
716      * @param canvas the canvas to draw on
717      * @param event the event to draw
718      * @param x the top left corner for this event's color chip
719      * @param y the top left corner for this event's color chip
720      * @param rightEdge the rightmost point we're allowed to draw on (exclusive)
721      * @param moreEvents indicates whether additional events will follow this one
722      * @param showTimes if set, a second line with a time range will be displayed for non-all-day
723      * events
724      * @param doDraw if set, do the actual drawing; otherwise this just computes the height
725      * and returns
726      * @return the y for the next event or the original y if it won't fit
727      */
drawEventnull728     protected fun drawEvent(canvas: Canvas, event: Event, x: Int, y: Int, rightEdge: Int,
729                             moreEvents: Boolean, showTimes: Boolean, doDraw: Boolean): Int {
730         /*
731          * Vertical layout:
732          *   (top of box)
733          * a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent
734          * b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event
735          * c. [optional] Time range (mExtrasHeight)
736          * d. EVENT_LINE_PADDING
737          *
738          * Repeat (b,c,d) as needed and space allows.  If we have more events than fit, we need
739          * to leave room for something like "+2" at the bottom:
740          *
741          * e. "+ more" line (mExtrasHeight)
742          *
743          * f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING)
744          *   (bottom of box)
745          */
746         var y = y
747         val BORDER_SPACE = EVENT_SQUARE_BORDER + 1 // want a 1-pixel gap inside border
748         val STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2 // adjust bounds for stroke width
749         val allDay: Boolean = event.allDay
750         var eventRequiredSpace = mEventHeight
751         if (allDay) {
752             // Add a few pixels for the box we draw around all-day events.
753             eventRequiredSpace += BORDER_SPACE * 2
754         } else if (showTimes) {
755             // Need room for the "1pm - 2pm" line.
756             eventRequiredSpace += mExtrasHeight
757         }
758         var reservedSpace = EVENT_BOTTOM_PADDING // leave a bit of room at the bottom
759         if (moreEvents) {
760             // More events follow.  Leave a bit of space between events.
761             eventRequiredSpace += EVENT_LINE_PADDING
762 
763             // Make sure we have room for the "+ more" line.  (The "+ more" line is expected
764             // to be <= the height of an event line, so we won't show "+1" when we could be
765             // showing the event.)
766             reservedSpace += mExtrasHeight
767         }
768         if (y + eventRequiredSpace + reservedSpace > mHeight) {
769             // Not enough space, return original y
770             return y
771         } else if (!doDraw) {
772             return y + eventRequiredSpace
773         }
774         val isDeclined = event.selfAttendeeStatus === Attendees.ATTENDEE_STATUS_DECLINED
775         var color: Int = event.color
776         if (isDeclined) {
777             color = Utils.getDeclinedColorFromColor(color)
778         }
779         val textX: Int
780         var textY: Int
781         val textRightEdge: Int
782         if (allDay) {
783             // We shift the render offset "inward", because drawRect with a stroke width greater
784             // than 1 draws outside the specified bounds.  (We don't adjust the left edge, since
785             // we want to match the existing appearance of the "event square".)
786             r.left = x
787             r.right = rightEdge - STROKE_WIDTH_ADJ
788             r.top = y + STROKE_WIDTH_ADJ
789             r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ
790             textX = x + BORDER_SPACE
791             textY = y + mEventAscentHeight + BORDER_SPACE
792             textRightEdge = rightEdge - BORDER_SPACE
793         } else {
794             r.left = x
795             r.right = x + EVENT_SQUARE_WIDTH
796             r.bottom = y + mEventAscentHeight
797             r.top = r.bottom - EVENT_SQUARE_WIDTH
798             textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING
799             textY = y + mEventAscentHeight
800             textRightEdge = rightEdge
801         }
802         var boxStyle: Style = Style.STROKE
803         var solidBackground = false
804         if (event.selfAttendeeStatus !== Attendees.ATTENDEE_STATUS_INVITED) {
805             boxStyle = Style.FILL_AND_STROKE
806             if (allDay) {
807                 solidBackground = true
808             }
809         }
810         mEventSquarePaint.setStyle(boxStyle)
811         mEventSquarePaint.setColor(color)
812         canvas.drawRect(r, mEventSquarePaint)
813         val avail = (textRightEdge - textX).toFloat()
814         var text: CharSequence = TextUtils.ellipsize(
815                 event.title, mEventPaint, avail, TextUtils.TruncateAt.END)
816         val textPaint: TextPaint?
817         textPaint = if (solidBackground) {
818             // Text color needs to contrast with solid background.
819             mSolidBackgroundEventPaint
820         } else if (isDeclined) {
821             // Use "declined event" color.
822             mDeclinedEventPaint
823         } else if (allDay) {
824             // Text inside frame is same color as frame.
825             mFramedEventPaint?.setColor(color)
826             mFramedEventPaint
827         } else {
828             // Use generic event text color.
829             mEventPaint
830         }
831         canvas.drawText(text.toString(), textX.toFloat(), textY.toFloat(), textPaint as Paint)
832         y += mEventHeight
833         if (allDay) {
834             y += BORDER_SPACE * 2
835         }
836         if (showTimes && !allDay) {
837             // show start/end time, e.g. "1pm - 2pm"
838             textY = y + mExtrasAscentHeight
839             mStringBuilder.setLength(0)
840             text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis,
841                     event.endMillis, DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
842                     Utils.getTimeZone(getContext(), null)).toString()
843             text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END)
844             canvas.drawText(text.toString(), textX.toFloat(), textY.toFloat(),
845                     if (isDeclined) mEventDeclinedExtrasPaint else mEventExtrasPaint)
846             y += mExtrasHeight
847         }
848         y += EVENT_LINE_PADDING
849         return y
850     }
851 
drawMoreEventsnull852     protected fun drawMoreEvents(canvas: Canvas, remainingEvents: Int, x: Int) {
853         val y: Int = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING)
854         val text: String = getContext().getResources().getQuantityString(
855                 R.plurals.month_more_events, remainingEvents)
856         mEventExtrasPaint.setAntiAlias(true)
857         mEventExtrasPaint.setFakeBoldText(true)
858         canvas.drawText(String.format(text, remainingEvents), x.toFloat(), y.toFloat(),
859                 mEventExtrasPaint as Paint)
860         mEventExtrasPaint.setFakeBoldText(false)
861     }
862 
863     /**
864      * Draws a line showing busy times in each day of week The method draws
865      * non-conflicting times in the event color and times with conflicting
866      * events in the dna conflict color defined in colors.
867      *
868      * @param canvas
869      */
drawDNAnull870     protected fun drawDNA(canvas: Canvas) {
871         // Draw event and conflict times
872         if (mDna != null) {
873             for (strand in mDna!!.values) {
874                 if (strand.color === CONFLICT_COLOR || strand.points == null ||
875                         (strand.points as FloatArray).size === 0) {
876                     continue
877                 }
878                 mDNATimePaint.setColor(strand.color)
879                 canvas.drawLines(strand.points as FloatArray, mDNATimePaint as Paint)
880             }
881             // Draw black last to make sure it's on top
882             val strand: Utils.DNAStrand? = mDna?.get(CONFLICT_COLOR)
883             if (strand != null && strand.points != null && strand.points?.size !== 0) {
884                 mDNATimePaint.setColor(strand.color)
885                 canvas.drawLines(strand.points as FloatArray, mDNATimePaint as Paint)
886             }
887             if (mDayXs == null) {
888                 return
889             }
890             val numDays = mDayXs!!.size
891             val xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2
892             if (strand != null && strand.allDays != null && strand.allDays?.size === numDays) {
893                 for (i in 0 until numDays) {
894                     // this adds at most 7 draws. We could sort it by color and
895                     // build an array instead but this is easier.
896                     if (strand.allDays?.get(i) !== 0) {
897                         mDNAAllDayPaint.setColor(strand.allDays!!.get(i))
898                         canvas.drawLine(mDayXs!![i].toFloat() + xOffset.toFloat(),
899                                 DNA_MARGIN.toFloat(), mDayXs!![i].toFloat() + xOffset.toFloat(),
900                                 DNA_MARGIN.toFloat() + DNA_ALL_DAY_HEIGHT.toFloat(),
901                                 mDNAAllDayPaint as Paint)
902                     }
903                 }
904             }
905         }
906     }
907 
908     @Override
updateSelectionPositionsnull909     protected override fun updateSelectionPositions() {
910         if (mHasSelectedDay) {
911             var selectedPosition: Int = mSelectedDay - mWeekStart
912             if (selectedPosition < 0) {
913                 selectedPosition += 7
914             }
915             var effectiveWidth: Int = mWidth - mPadding * 2
916             effectiveWidth -= SPACING_WEEK_NUMBER
917             mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding
918             mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding
919             mSelectedLeft += SPACING_WEEK_NUMBER
920             mSelectedRight += SPACING_WEEK_NUMBER
921         }
922     }
923 
getDayIndexFromLocationnull924     fun getDayIndexFromLocation(x: Float): Int {
925         val dayStart: Int = if (mShowWeekNum) SPACING_WEEK_NUMBER + mPadding else mPadding
926         return if (x < dayStart || x > mWidth - mPadding) {
927             -1
928         } else (((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)).toInt())
929         // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
930     }
931 
932     @Override
getDayFromLocationnull933     override fun getDayFromLocation(x: Float): Time? {
934         val dayPosition = getDayIndexFromLocation(x)
935         if (dayPosition == -1) {
936             return null
937         }
938         var day: Int = mFirstJulianDay + dayPosition
939         val time = Time(mTimeZone)
940         if (mWeek === 0) {
941             // This week is weird...
942             if (day < Time.EPOCH_JULIAN_DAY) {
943                 day++
944             } else if (day == Time.EPOCH_JULIAN_DAY) {
945                 time.set(1, 0, 1970)
946                 time.normalize(true)
947                 return time
948             }
949         }
950         time.setJulianDay(day)
951         return time
952     }
953 
954     @Override
onHoverEventnull955     override fun onHoverEvent(event: MotionEvent): Boolean {
956         val context: Context = getContext()
957         // only send accessibility events if accessibility and exploration are
958         // on.
959         val am: AccessibilityManager = context
960                 .getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager
961         if (!am.isEnabled() || !am.isTouchExplorationEnabled()) {
962             return super.onHoverEvent(event)
963         }
964         if (event.getAction() !== MotionEvent.ACTION_HOVER_EXIT) {
965             val hover: Time? = getDayFromLocation(event.getX())
966             if (hover != null
967                     && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) !== 0)) {
968                 val millis: Long = hover.toMillis(true)
969                 val date: String = Utils.formatDateRange(context, millis, millis,
970                         DateUtils.FORMAT_SHOW_DATE) as String
971                 val accessEvent: AccessibilityEvent = AccessibilityEvent
972                         .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)
973                 accessEvent.getText().add(date)
974                 if (mShowDetailsInMonth && mEvents != null) {
975                     val dayStart: Int = SPACING_WEEK_NUMBER + mPadding
976                     val dayPosition = ((event.getX() - dayStart) * mNumDays / (mWidth
977                             - dayStart - mPadding)).toInt()
978                     val events: ArrayList<Event?> = mEvents!![dayPosition]
979                     val text: List<CharSequence> = accessEvent.getText() as List<CharSequence>
980                     for (e in events) {
981                         text.add(e!!.titleAndLocation.toString() + ". ")
982                         var flags: Int = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR
983                         if (!e.allDay) {
984                             flags = flags or DateUtils.FORMAT_SHOW_TIME
985                             if (DateFormat.is24HourFormat(context)) {
986                                 flags = flags or DateUtils.FORMAT_24HOUR
987                             }
988                         } else {
989                             flags = flags or DateUtils.FORMAT_UTC
990                         }
991                         text.add(Utils.formatDateRange(context, e.startMillis, e.endMillis,
992                                 flags).toString() + ". ")
993                     }
994                 }
995                 sendAccessibilityEventUnchecked(accessEvent)
996                 mLastHoverTime = hover
997             }
998         }
999         return true
1000     }
1001 
setClickedDaynull1002     fun setClickedDay(xLocation: Float) {
1003         mClickedDayIndex = getDayIndexFromLocation(xLocation)
1004         invalidate()
1005     }
1006 
clearClickedDaynull1007     fun clearClickedDay() {
1008         mClickedDayIndex = -1
1009         invalidate()
1010     }
1011 
1012     companion object {
1013         private const val TAG = "MonthView"
1014         private const val DEBUG_LAYOUT = false
1015         const val VIEW_PARAMS_ORIENTATION = "orientation"
1016         const val VIEW_PARAMS_ANIMATE_TODAY = "animate_today"
1017 
1018         /* NOTE: these are not constants, and may be multiplied by a scale factor */
1019         private var TEXT_SIZE_MONTH_NUMBER = 32
1020         private var TEXT_SIZE_EVENT = 12
1021         private var TEXT_SIZE_EVENT_TITLE = 14
1022         private var TEXT_SIZE_MORE_EVENTS = 12
1023         private var TEXT_SIZE_MONTH_NAME = 14
1024         private var TEXT_SIZE_WEEK_NUM = 12
1025         private var DNA_MARGIN = 4
1026         private var DNA_ALL_DAY_HEIGHT = 4
1027         private var DNA_MIN_SEGMENT_HEIGHT = 4
1028         private var DNA_WIDTH = 8
1029         private var DNA_ALL_DAY_WIDTH = 32
1030         private var DNA_SIDE_PADDING = 6
1031         private var CONFLICT_COLOR: Int = Color.BLACK
1032         private var EVENT_TEXT_COLOR: Int = Color.WHITE
1033         private var DEFAULT_EDGE_SPACING = 0
1034         private var SIDE_PADDING_MONTH_NUMBER = 4
1035         private var TOP_PADDING_MONTH_NUMBER = 4
1036         private var TOP_PADDING_WEEK_NUMBER = 4
1037         private var SIDE_PADDING_WEEK_NUMBER = 20
1038         private var DAY_SEPARATOR_OUTER_WIDTH = 0
1039         private var DAY_SEPARATOR_INNER_WIDTH = 1
1040         private var DAY_SEPARATOR_VERTICAL_LENGTH = 53
1041         private var DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT = 64
1042         private const val MIN_WEEK_WIDTH = 50
1043         private var EVENT_X_OFFSET_LANDSCAPE = 38
1044         private var EVENT_Y_OFFSET_LANDSCAPE = 8
1045         private var EVENT_Y_OFFSET_PORTRAIT = 7
1046         private var EVENT_SQUARE_WIDTH = 10
1047         private var EVENT_SQUARE_BORDER = 2
1048         private var EVENT_LINE_PADDING = 2
1049         private var EVENT_RIGHT_PADDING = 4
1050         private var EVENT_BOTTOM_PADDING = 3
1051         private var TODAY_HIGHLIGHT_WIDTH = 2
1052         private var SPACING_WEEK_NUMBER = 24
1053         private var mInitialized = false
1054         private var mShowDetailsInMonth = false
1055         protected var mStringBuilder: StringBuilder = StringBuilder(50)
1056 
1057         // TODO recreate formatter when locale changes
1058         protected var mFormatter: Formatter = Formatter(mStringBuilder, Locale.getDefault())
1059         private const val mClickedAlpha = 128
1060     }
1061 }