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
17 
18 import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME
19 import android.provider.CalendarContract.EXTRA_EVENT_END_TIME
20 import android.provider.CalendarContract.Attendees.ATTENDEE_STATUS
21 import android.content.ComponentName
22 import android.content.ContentUris
23 import android.content.Context
24 import android.content.Intent
25 import android.net.Uri
26 import android.provider.CalendarContract.Attendees
27 import android.provider.CalendarContract.Events
28 import android.text.format.Time
29 import android.util.Log
30 import android.util.Pair
31 import java.lang.ref.WeakReference
32 import java.util.LinkedHashMap
33 import java.util.LinkedList
34 import java.util.WeakHashMap
35 
36 class CalendarController private constructor(context: Context?) {
37     private var mContext: Context? = null
38 
39     // This uses a LinkedHashMap so that we can replace fragments based on the
40     // view id they are being expanded into since we can't guarantee a reference
41     // to the handler will be findable
42     private val eventHandlers: LinkedHashMap<Int, EventHandler> =
43         LinkedHashMap<Int, EventHandler>(5)
44     private val mToBeRemovedEventHandlers: LinkedList<Int> = LinkedList<Int>()
45     private val mToBeAddedEventHandlers: LinkedHashMap<Int, EventHandler> =
46         LinkedHashMap<Int, EventHandler>()
47     private var mFirstEventHandler: Pair<Int, EventHandler>? = null
48     private var mToBeAddedFirstEventHandler: Pair<Int, EventHandler>? = null
49 
50     @Volatile
51     private var mDispatchInProgressCounter = 0
52     private val filters: WeakHashMap<Object, Long> = WeakHashMap<Object, Long>(1)
53 
54     // Forces the viewType. Should only be used for initialization.
55     var viewType = -1
56     private var mDetailViewType = -1
57     var previousViewType = -1
58         private set
59 
60     // The last event ID the edit view was launched with
61     var eventId: Long = -1
62     private val mTime: Time? = Time()
63 
64     // The last set of date flags sent with
65     var dateFlags: Long = 0
66         private set
67     private val mUpdateTimezone: Runnable = object : Runnable {
68         @Override
runnull69         override fun run() {
70             mTime?.switchTimezone(Utils.getTimeZone(mContext, this))
71         }
72     }
73 
74     /**
75      * One of the event types that are sent to or from the controller
76      */
77     interface EventType {
78         companion object {
79             // Simple view of an event
80             const val VIEW_EVENT = 1L shl 1
81 
82             // Full detail view in read only mode
83             const val VIEW_EVENT_DETAILS = 1L shl 2
84 
85             // full detail view in edit mode
86             const val EDIT_EVENT = 1L shl 3
87             const val GO_TO = 1L shl 5
88             const val EVENTS_CHANGED = 1L shl 7
89             const val USER_HOME = 1L shl 9
90 
91             // date range has changed, update the title
92             const val UPDATE_TITLE = 1L shl 10
93         }
94     }
95 
96     /**
97      * One of the Agenda/Day/Week/Month view types
98      */
99     interface ViewType {
100         companion object {
101             const val DETAIL = -1
102             const val CURRENT = 0
103             const val AGENDA = 1
104             const val DAY = 2
105             const val WEEK = 3
106             const val MONTH = 4
107             const val EDIT = 5
108             const val MAX_VALUE = 5
109         }
110     }
111 
112     class EventInfo {
113         @JvmField var eventType: Long = 0 // one of the EventType
114         @JvmField var viewType = 0 // one of the ViewType
115         @JvmField var id: Long = 0 // event id
116         @JvmField var selectedTime: Time? = null // the selected time in focus
117 
118         // Event start and end times.  All-day events are represented in:
119         // - local time for GO_TO commands
120         // - UTC time for VIEW_EVENT and other event-related commands
121         @JvmField var startTime: Time? = null
122         @JvmField var endTime: Time? = null
123         @JvmField var x = 0 // x coordinate in the activity space
124         @JvmField var y = 0 // y coordinate in the activity space
125         @JvmField var query: String? = null // query for a user search
126         @JvmField var componentName: ComponentName? = null // used in combination with query
127         @JvmField var eventTitle: String? = null
128         @JvmField var calendarId: Long = 0
129 
130         /**
131          * For EventType.VIEW_EVENT:
132          * It is the default attendee response and an all day event indicator.
133          * Set to Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED,
134          * Attendees.ATTENDEE_STATUS_DECLINED, or Attendees.ATTENDEE_STATUS_TENTATIVE.
135          * To signal the event is an all-day event, "or" ALL_DAY_MASK with the response.
136          * Alternatively, use buildViewExtraLong(), getResponse(), and isAllDay().
137          *
138          *
139          * For EventType.GO_TO:
140          * Set to [.EXTRA_GOTO_TIME] to go to the specified date/time.
141          * Set to [.EXTRA_GOTO_DATE] to consider the date but ignore the time.
142          * Set to [.EXTRA_GOTO_BACK_TO_PREVIOUS] if back should bring back previous view.
143          * Set to [.EXTRA_GOTO_TODAY] if this is a user request to go to the current time.
144          *
145          *
146          * For EventType.UPDATE_TITLE:
147          * Set formatting flags for Utils.formatDateRange
148          */
149         @JvmField var extraLong: Long = 0
150         val isAllDay: Boolean
151             get() {
152                 if (eventType != EventType.VIEW_EVENT) {
153                     Log.wtf(TAG, "illegal call to isAllDay , wrong event type $eventType")
154                     return false
155                 }
156                 return if (extraLong and ALL_DAY_MASK != 0L) true else false
157             }
158         val response: Int
159             get() {
160                 if (eventType != EventType.VIEW_EVENT) {
161                     Log.wtf(TAG, "illegal call to getResponse , wrong event type $eventType")
162                     return Attendees.ATTENDEE_STATUS_NONE
163                 }
164                 val response = (extraLong and ATTENTEE_STATUS_MASK).toInt()
165                 when (response) {
166                     ATTENDEE_STATUS_NONE_MASK -> return Attendees.ATTENDEE_STATUS_NONE
167                     ATTENDEE_STATUS_ACCEPTED_MASK -> return Attendees.ATTENDEE_STATUS_ACCEPTED
168                     ATTENDEE_STATUS_DECLINED_MASK -> return Attendees.ATTENDEE_STATUS_DECLINED
169                     ATTENDEE_STATUS_TENTATIVE_MASK -> return Attendees.ATTENDEE_STATUS_TENTATIVE
170                     else -> Log.wtf(TAG, "Unknown attendee response $response")
171                 }
172                 return ATTENDEE_STATUS_NONE_MASK
173             }
174 
175         companion object {
176             private const val ATTENTEE_STATUS_MASK: Long = 0xFF
177             private const val ALL_DAY_MASK: Long = 0x100
178             private const val ATTENDEE_STATUS_NONE_MASK = 0x01
179             private const val ATTENDEE_STATUS_ACCEPTED_MASK = 0x02
180             private const val ATTENDEE_STATUS_DECLINED_MASK = 0x04
181             private const val ATTENDEE_STATUS_TENTATIVE_MASK = 0x08
182 
183             // Used to build the extra long for a VIEW event.
buildViewExtraLongnull184             @JvmStatic fun buildViewExtraLong(response: Int, allDay: Boolean): Long {
185                 var extra = if (allDay) ALL_DAY_MASK else 0
186                 extra = when (response) {
187                     Attendees.ATTENDEE_STATUS_NONE -> extra or
188                             ATTENDEE_STATUS_NONE_MASK.toLong()
189                     Attendees.ATTENDEE_STATUS_ACCEPTED -> extra or
190                             ATTENDEE_STATUS_ACCEPTED_MASK.toLong()
191                     Attendees.ATTENDEE_STATUS_DECLINED -> extra or
192                             ATTENDEE_STATUS_DECLINED_MASK.toLong()
193                     Attendees.ATTENDEE_STATUS_TENTATIVE -> extra or
194                             ATTENDEE_STATUS_TENTATIVE_MASK.toLong()
195                     else -> {
196                         Log.wtf(
197                                 TAG,
198                                 "Unknown attendee response $response"
199                         )
200                         extra or ATTENDEE_STATUS_NONE_MASK.toLong()
201                     }
202                 }
203                 return extra
204             }
205         }
206     }
207 
208     interface EventHandler {
209         val supportedEventTypes: Long
handleEventnull210         fun handleEvent(event: EventInfo?)
211 
212         /**
213          * This notifies the handler that the database has changed and it should
214          * update its view.
215          */
216         fun eventsChanged()
217     }
218 
219     fun sendEventRelatedEvent(
220         sender: Object?,
221         eventType: Long,
222         eventId: Long,
223         startMillis: Long,
224         endMillis: Long,
225         x: Int,
226         y: Int,
227         selectedMillis: Long
228     ) {
229         // TODO: pass the real allDay status or at least a status that says we don't know the
230         // status and have the receiver query the data.
231         // The current use of this method for VIEW_EVENT is by the day view to show an EventInfo
232         // so currently the missing allDay status has no effect.
233         sendEventRelatedEventWithExtra(
234                 sender, eventType, eventId, startMillis, endMillis, x, y,
235                 EventInfo.buildViewExtraLong(Attendees.ATTENDEE_STATUS_NONE, false),
236                 selectedMillis
237         )
238     }
239 
240     /**
241      * Helper for sending New/View/Edit/Delete events
242      *
243      * @param sender object of the caller
244      * @param eventType one of [EventType]
245      * @param eventId event id
246      * @param startMillis start time
247      * @param endMillis end time
248      * @param x x coordinate in the activity space
249      * @param y y coordinate in the activity space
250      * @param extraLong default response value for the "simple event view" and all day indication.
251      * Use Attendees.ATTENDEE_STATUS_NONE for no response.
252      * @param selectedMillis The time to specify as selected
253      */
sendEventRelatedEventWithExtranull254     fun sendEventRelatedEventWithExtra(
255         sender: Object?,
256         eventType: Long,
257         eventId: Long,
258         startMillis: Long,
259         endMillis: Long,
260         x: Int,
261         y: Int,
262         extraLong: Long,
263         selectedMillis: Long
264     ) {
265         sendEventRelatedEventWithExtraWithTitleWithCalendarId(
266                 sender, eventType, eventId,
267                 startMillis, endMillis, x, y, extraLong, selectedMillis, null, -1
268         )
269     }
270 
271     /**
272      * Helper for sending New/View/Edit/Delete events
273      *
274      * @param sender object of the caller
275      * @param eventType one of [EventType]
276      * @param eventId event id
277      * @param startMillis start time
278      * @param endMillis end time
279      * @param x x coordinate in the activity space
280      * @param y y coordinate in the activity space
281      * @param extraLong default response value for the "simple event view" and all day indication.
282      * Use Attendees.ATTENDEE_STATUS_NONE for no response.
283      * @param selectedMillis The time to specify as selected
284      * @param title The title of the event
285      * @param calendarId The id of the calendar which the event belongs to
286      */
sendEventRelatedEventWithExtraWithTitleWithCalendarIdnull287     fun sendEventRelatedEventWithExtraWithTitleWithCalendarId(
288         sender: Object?,
289         eventType: Long,
290         eventId: Long,
291         startMillis: Long,
292         endMillis: Long,
293         x: Int,
294         y: Int,
295         extraLong: Long,
296         selectedMillis: Long,
297         title: String?,
298         calendarId: Long
299     ) {
300         val info = EventInfo()
301         info.eventType = eventType
302         if (eventType == EventType.VIEW_EVENT_DETAILS) {
303             info.viewType = ViewType.CURRENT
304         }
305         info.id = eventId
306         info.startTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone))
307         (info.startTime as Time).set(startMillis)
308         if (selectedMillis != -1L) {
309             info.selectedTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone))
310             (info.selectedTime as Time).set(selectedMillis)
311         } else {
312             info.selectedTime = info.startTime
313         }
314         info.endTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone))
315         (info.endTime as Time).set(endMillis)
316         info.x = x
317         info.y = y
318         info.extraLong = extraLong
319         info.eventTitle = title
320         info.calendarId = calendarId
321         this.sendEvent(sender, info)
322     }
323 
324     /**
325      * Helper for sending non-calendar-event events
326      *
327      * @param sender object of the caller
328      * @param eventType one of [EventType]
329      * @param start start time
330      * @param end end time
331      * @param eventId event id
332      * @param viewType [ViewType]
333      */
sendEventnull334     fun sendEvent(
335         sender: Object?,
336         eventType: Long,
337         start: Time?,
338         end: Time?,
339         eventId: Long,
340         viewType: Int
341     ) {
342         sendEvent(
343                 sender, eventType, start, end, start, eventId, viewType, EXTRA_GOTO_TIME, null,
344                 null
345         )
346     }
347 
348     /**
349      * sendEvent() variant with extraLong, search query, and search component name.
350      */
sendEventnull351     fun sendEvent(
352         sender: Object?,
353         eventType: Long,
354         start: Time?,
355         end: Time?,
356         eventId: Long,
357         viewType: Int,
358         extraLong: Long,
359         query: String?,
360         componentName: ComponentName?
361     ) {
362         sendEvent(
363                 sender, eventType, start, end, start, eventId, viewType, extraLong, query,
364                 componentName
365         )
366     }
367 
sendEventnull368     fun sendEvent(
369         sender: Object?,
370         eventType: Long,
371         start: Time?,
372         end: Time?,
373         selected: Time?,
374         eventId: Long,
375         viewType: Int,
376         extraLong: Long,
377         query: String?,
378         componentName: ComponentName?
379     ) {
380         val info = EventInfo()
381         info.eventType = eventType
382         info.startTime = start
383         info.selectedTime = selected
384         info.endTime = end
385         info.id = eventId
386         info.viewType = viewType
387         info.query = query
388         info.componentName = componentName
389         info.extraLong = extraLong
390         this.sendEvent(sender, info)
391     }
392 
sendEventnull393     fun sendEvent(sender: Object?, event: EventInfo) {
394         // TODO Throw exception on invalid events
395         if (DEBUG) {
396             Log.d(TAG, eventInfoToString(event))
397         }
398         val filteredTypes: Long? = filters.get(sender)
399         if (filteredTypes != null && filteredTypes.toLong() and event.eventType != 0L) {
400             // Suppress event per filter
401             if (DEBUG) {
402                 Log.d(TAG, "Event suppressed")
403             }
404             return
405         }
406         previousViewType = viewType
407 
408         // Fix up view if not specified
409         if (event.viewType == ViewType.DETAIL) {
410             event.viewType = mDetailViewType
411             viewType = mDetailViewType
412         } else if (event.viewType == ViewType.CURRENT) {
413             event.viewType = viewType
414         } else if (event.viewType != ViewType.EDIT) {
415             viewType = event.viewType
416             if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY ||
417                     Utils.getAllowWeekForDetailView() && event.viewType == ViewType.WEEK) {
418                 mDetailViewType = viewType
419             }
420         }
421         if (DEBUG) {
422             Log.d(TAG, "vvvvvvvvvvvvvvv")
423             Log.d(
424                     TAG,
425                     "Start  " + if (event.startTime == null) "null" else event.startTime.toString()
426             )
427             Log.d(TAG, "End    " + if (event.endTime == null) "null" else event.endTime.toString())
428             Log.d(
429                     TAG,
430                     "Select " + if (event.selectedTime == null) "null"
431                     else event.selectedTime.toString()
432             )
433             Log.d(TAG, "mTime  " + if (mTime == null) "null" else mTime.toString())
434         }
435         var startMillis: Long = 0
436         val temp = event.startTime
437         if (temp != null) {
438             startMillis = (event.startTime as Time).toMillis(false)
439         }
440 
441         // Set mTime if selectedTime is set
442         val temp1 = event.selectedTime
443         if (temp1 != null && temp1.toMillis(false) != 0L) {
444             mTime?.set(event.selectedTime)
445         } else {
446             if (startMillis != 0L) {
447                 // selectedTime is not set so set mTime to startTime iff it is not
448                 // within start and end times
449                 val mtimeMillis: Long = mTime?.toMillis(false) as Long
450                 val temp2 = event.endTime
451                 if (mtimeMillis < startMillis ||
452                         temp2 != null && mtimeMillis > temp2.toMillis(false)) {
453                     mTime.set(event.startTime)
454                 }
455             }
456             event.selectedTime = mTime
457         }
458         // Store the formatting flags if this is an update to the title
459         if (event.eventType == EventType.UPDATE_TITLE) {
460             dateFlags = event.extraLong
461         }
462 
463         // Fix up start time if not specified
464         if (startMillis == 0L) {
465             event.startTime = mTime
466         }
467         if (DEBUG) {
468             Log.d(
469                     TAG,
470                     "Start  " + if (event.startTime == null) "null" else
471                         event.startTime.toString()
472             )
473             Log.d(TAG, "End    " + if (event.endTime == null) "null" else
474                 event.endTime.toString())
475             Log.d(
476                     TAG,
477                     "Select " + if (event.selectedTime == null) "null" else
478                         event.selectedTime.toString()
479             )
480             Log.d(TAG, "mTime  " + if (mTime == null) "null" else mTime.toString())
481             Log.d(TAG, "^^^^^^^^^^^^^^^")
482         }
483 
484         // Store the eventId if we're entering edit event
485         if ((event.eventType and EventType.VIEW_EVENT_DETAILS) != 0L) {
486             if (event.id > 0) {
487                 eventId = event.id
488             } else {
489                 eventId = -1
490             }
491         }
492         var handled = false
493         synchronized(this) {
494             mDispatchInProgressCounter++
495             if (DEBUG) {
496                 Log.d(
497                         TAG,
498                         "sendEvent: Dispatching to " + eventHandlers.size.toString() + " handlers"
499                 )
500             }
501             // Dispatch to event handler(s)
502             val temp3 = mFirstEventHandler
503             if (temp3 != null) {
504                 // Handle the 'first' one before handling the others
505                 val handler: EventHandler? = mFirstEventHandler?.second
506                 if (handler != null && handler.supportedEventTypes and event.eventType != 0L &&
507                         !mToBeRemovedEventHandlers.contains(mFirstEventHandler?.first)) {
508                     handler.handleEvent(event)
509                     handled = true
510                 }
511             }
512             val handlers: MutableIterator<MutableMap.MutableEntry<Int,
513                 CalendarController.EventHandler>> = eventHandlers.entries.iterator()
514             while (handlers.hasNext()) {
515                 val entry: MutableMap.MutableEntry<Int,
516                     CalendarController.EventHandler> = handlers.next()
517                 val key: Int = entry.key.toInt()
518                 val temp4 = mFirstEventHandler
519                 if (temp4 != null && key.toInt() == temp4.first.toInt()) {
520                     // If this was the 'first' handler it was already handled
521                     continue
522                 }
523                 val eventHandler: EventHandler = entry.value
524                 if (eventHandler != null &&
525                     eventHandler.supportedEventTypes and event.eventType != 0L) {
526                     if (mToBeRemovedEventHandlers.contains(key)) {
527                         continue
528                     }
529                     eventHandler.handleEvent(event)
530                     handled = true
531                 }
532             }
533             mDispatchInProgressCounter--
534             if (mDispatchInProgressCounter == 0) {
535 
536                 // Deregister removed handlers
537                 if (mToBeRemovedEventHandlers.size > 0) {
538                     for (zombie in mToBeRemovedEventHandlers) {
539                         eventHandlers.remove(zombie)
540                         val temp5 = mFirstEventHandler
541                         if (temp5 != null && zombie.equals(temp5.first)) {
542                             mFirstEventHandler = null
543                         }
544                     }
545                     mToBeRemovedEventHandlers.clear()
546                 }
547                 // Add new handlers
548                 if (mToBeAddedFirstEventHandler != null) {
549                     mFirstEventHandler = mToBeAddedFirstEventHandler
550                     mToBeAddedFirstEventHandler = null
551                 }
552                 if (mToBeAddedEventHandlers.size > 0) {
553                     for (food in mToBeAddedEventHandlers.entries) {
554                         eventHandlers.put(food.key, food.value)
555                     }
556                 }
557             }
558         }
559     }
560 
561     /**
562      * Adds or updates an event handler. This uses a LinkedHashMap so that we can
563      * replace fragments based on the view id they are being expanded into.
564      *
565      * @param key The view id or placeholder for this handler
566      * @param eventHandler Typically a fragment or activity in the calendar app
567      */
registerEventHandlernull568     fun registerEventHandler(key: Int, eventHandler: EventHandler?) {
569         synchronized(this) {
570             if (mDispatchInProgressCounter > 0) {
571                 mToBeAddedEventHandlers.put(key,
572                         eventHandler as CalendarController.EventHandler)
573             } else {
574                 eventHandlers.put(key, eventHandler as CalendarController.EventHandler)
575             }
576         }
577     }
578 
registerFirstEventHandlernull579     fun registerFirstEventHandler(key: Int, eventHandler: EventHandler?) {
580         synchronized(this) {
581             registerEventHandler(key, eventHandler)
582             if (mDispatchInProgressCounter > 0) {
583                 mToBeAddedFirstEventHandler = Pair<Int, EventHandler>(key, eventHandler)
584             } else {
585                 mFirstEventHandler = Pair<Int, EventHandler>(key, eventHandler)
586             }
587         }
588     }
589 
deregisterEventHandlernull590     fun deregisterEventHandler(key: Int) {
591         synchronized(this) {
592             if (mDispatchInProgressCounter > 0) {
593                 // To avoid ConcurrencyException, stash away the event handler for now.
594                 mToBeRemovedEventHandlers.add(key)
595             } else {
596                 eventHandlers.remove(key)
597                 val temp6 = mFirstEventHandler
598                 if (temp6 != null && temp6.first == key) {
599                     mFirstEventHandler = null
600                 } else {}
601             }
602         }
603     }
604 
deregisterAllEventHandlersnull605     fun deregisterAllEventHandlers() {
606         synchronized(this) {
607             if (mDispatchInProgressCounter > 0) {
608                 // To avoid ConcurrencyException, stash away the event handler for now.
609                 mToBeRemovedEventHandlers.addAll(eventHandlers.keys)
610             } else {
611                 eventHandlers.clear()
612                 mFirstEventHandler = null
613             }
614         }
615     }
616 
617     // FRAG_TODO doesn't work yet
filterBroadcastsnull618     fun filterBroadcasts(sender: Object?, eventTypes: Long) {
619         filters.put(sender, eventTypes)
620     }
621     /**
622      * @return the time that this controller is currently pointed at
623      */
624     /**
625      * Set the time this controller is currently pointed at
626      *
627      * @param millisTime Time since epoch in millis
628      */
629     var time: Long?
630         get() = mTime?.toMillis(false)
631         set(millisTime) {
632             mTime?.set(millisTime as Long)
633         }
634 
launchViewEventnull635     fun launchViewEvent(eventId: Long, startMillis: Long, endMillis: Long, response: Int) {
636         val intent = Intent(Intent.ACTION_VIEW)
637         val eventUri: Uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId)
638         intent.setData(eventUri)
639         intent.setClass(mContext as Context, AllInOneActivity::class.java)
640         intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis)
641         intent.putExtra(EXTRA_EVENT_END_TIME, endMillis)
642         intent.putExtra(ATTENDEE_STATUS, response)
643         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
644         mContext?.startActivity(intent)
645     }
646 
eventInfoToStringnull647     private fun eventInfoToString(eventInfo: EventInfo): String {
648         var tmp = "Unknown"
649         val builder = StringBuilder()
650         if (eventInfo.eventType and EventType.GO_TO != 0L) {
651             tmp = "Go to time/event"
652         } else if (eventInfo.eventType and EventType.VIEW_EVENT != 0L) {
653             tmp = "View event"
654         } else if (eventInfo.eventType and EventType.VIEW_EVENT_DETAILS != 0L) {
655             tmp = "View details"
656         } else if (eventInfo.eventType and EventType.EVENTS_CHANGED != 0L) {
657             tmp = "Refresh events"
658         } else if (eventInfo.eventType and EventType.USER_HOME != 0L) {
659             tmp = "Gone home"
660         } else if (eventInfo.eventType and EventType.UPDATE_TITLE != 0L) {
661             tmp = "Update title"
662         }
663         builder.append(tmp)
664         builder.append(": id=")
665         builder.append(eventInfo.id)
666         builder.append(", selected=")
667         builder.append(eventInfo.selectedTime)
668         builder.append(", start=")
669         builder.append(eventInfo.startTime)
670         builder.append(", end=")
671         builder.append(eventInfo.endTime)
672         builder.append(", viewType=")
673         builder.append(eventInfo.viewType)
674         builder.append(", x=")
675         builder.append(eventInfo.x)
676         builder.append(", y=")
677         builder.append(eventInfo.y)
678         return builder.toString()
679     }
680 
681     companion object {
682         private const val DEBUG = false
683         private const val TAG = "CalendarController"
684         const val EVENT_EDIT_ON_LAUNCH = "editMode"
685         const val MIN_CALENDAR_YEAR = 1970
686         const val MAX_CALENDAR_YEAR = 2036
687         const val MIN_CALENDAR_WEEK = 0
688         const val MAX_CALENDAR_WEEK = 3497 // weeks between 1/1/1970 and 1/1/2037
689         private val instances: WeakHashMap<Context, WeakReference<CalendarController>> =
690                 WeakHashMap<Context, WeakReference<CalendarController>>()
691 
692         /**
693          * Pass to the ExtraLong parameter for EventType.GO_TO to signal the time
694          * can be ignored
695          */
696         const val EXTRA_GOTO_DATE: Long = 1
697         const val EXTRA_GOTO_TIME: Long = 2
698         const val EXTRA_GOTO_BACK_TO_PREVIOUS: Long = 4
699         const val EXTRA_GOTO_TODAY: Long = 8
700 
701         /**
702          * Creates and/or returns an instance of CalendarController associated with
703          * the supplied context. It is best to pass in the current Activity.
704          *
705          * @param context The activity if at all possible.
706          */
getInstancenull707         @JvmStatic fun getInstance(context: Context?): CalendarController? {
708             synchronized(instances) {
709                 var controller: CalendarController? = null
710                 val weakController: WeakReference<CalendarController>? = instances.get(context)
711                 if (weakController != null) {
712                     controller = weakController.get()
713                 }
714                 if (controller == null) {
715                     controller = CalendarController(context)
716                     instances.put(context, WeakReference(controller))
717                 }
718                 return controller
719             }
720         }
721 
722         /**
723          * Removes an instance when it is no longer needed. This should be called in
724          * an activity's onDestroy method.
725          *
726          * @param context The activity used to create the controller
727          */
removeInstancenull728         @JvmStatic fun removeInstance(context: Context?) {
729             instances.remove(context)
730         }
731     }
732 
733     init {
734         mContext = context
735         mUpdateTimezone.run()
736         mTime?.setToNow()
737         mDetailViewType = Utils.getSharedPreference(
738                 mContext,
739                 GeneralPreferences.KEY_DETAILED_VIEW,
740                 GeneralPreferences.DEFAULT_DETAILED_VIEW
741         )
742     }
743 }