diff options
Diffstat (limited to 'src/com/android/calendar/CalendarController.kt')
-rw-r--r-- | src/com/android/calendar/CalendarController.kt | 743 |
1 files changed, 743 insertions, 0 deletions
diff --git a/src/com/android/calendar/CalendarController.kt b/src/com/android/calendar/CalendarController.kt new file mode 100644 index 00000000..16ee8fdd --- /dev/null +++ b/src/com/android/calendar/CalendarController.kt @@ -0,0 +1,743 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.calendar + +import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME +import android.provider.CalendarContract.EXTRA_EVENT_END_TIME +import android.provider.CalendarContract.Attendees.ATTENDEE_STATUS +import android.content.ComponentName +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Events +import android.text.format.Time +import android.util.Log +import android.util.Pair +import java.lang.ref.WeakReference +import java.util.LinkedHashMap +import java.util.LinkedList +import java.util.WeakHashMap + +class CalendarController private constructor(context: Context?) { + private var mContext: Context? = null + + // This uses a LinkedHashMap so that we can replace fragments based on the + // view id they are being expanded into since we can't guarantee a reference + // to the handler will be findable + private val eventHandlers: LinkedHashMap<Int, EventHandler> = + LinkedHashMap<Int, EventHandler>(5) + private val mToBeRemovedEventHandlers: LinkedList<Int> = LinkedList<Int>() + private val mToBeAddedEventHandlers: LinkedHashMap<Int, EventHandler> = + LinkedHashMap<Int, EventHandler>() + private var mFirstEventHandler: Pair<Int, EventHandler>? = null + private var mToBeAddedFirstEventHandler: Pair<Int, EventHandler>? = null + + @Volatile + private var mDispatchInProgressCounter = 0 + private val filters: WeakHashMap<Object, Long> = WeakHashMap<Object, Long>(1) + + // Forces the viewType. Should only be used for initialization. + var viewType = -1 + private var mDetailViewType = -1 + var previousViewType = -1 + private set + + // The last event ID the edit view was launched with + var eventId: Long = -1 + private val mTime: Time? = Time() + + // The last set of date flags sent with + var dateFlags: Long = 0 + private set + private val mUpdateTimezone: Runnable = object : Runnable { + @Override + override fun run() { + mTime?.switchTimezone(Utils.getTimeZone(mContext, this)) + } + } + + /** + * One of the event types that are sent to or from the controller + */ + interface EventType { + companion object { + // Simple view of an event + const val VIEW_EVENT = 1L shl 1 + + // Full detail view in read only mode + const val VIEW_EVENT_DETAILS = 1L shl 2 + + // full detail view in edit mode + const val EDIT_EVENT = 1L shl 3 + const val GO_TO = 1L shl 5 + const val EVENTS_CHANGED = 1L shl 7 + const val USER_HOME = 1L shl 9 + + // date range has changed, update the title + const val UPDATE_TITLE = 1L shl 10 + } + } + + /** + * One of the Agenda/Day/Week/Month view types + */ + interface ViewType { + companion object { + const val DETAIL = -1 + const val CURRENT = 0 + const val AGENDA = 1 + const val DAY = 2 + const val WEEK = 3 + const val MONTH = 4 + const val EDIT = 5 + const val MAX_VALUE = 5 + } + } + + class EventInfo { + @JvmField var eventType: Long = 0 // one of the EventType + @JvmField var viewType = 0 // one of the ViewType + @JvmField var id: Long = 0 // event id + @JvmField var selectedTime: Time? = null // the selected time in focus + + // Event start and end times. All-day events are represented in: + // - local time for GO_TO commands + // - UTC time for VIEW_EVENT and other event-related commands + @JvmField var startTime: Time? = null + @JvmField var endTime: Time? = null + @JvmField var x = 0 // x coordinate in the activity space + @JvmField var y = 0 // y coordinate in the activity space + @JvmField var query: String? = null // query for a user search + @JvmField var componentName: ComponentName? = null // used in combination with query + @JvmField var eventTitle: String? = null + @JvmField var calendarId: Long = 0 + + /** + * For EventType.VIEW_EVENT: + * It is the default attendee response and an all day event indicator. + * Set to Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED, + * Attendees.ATTENDEE_STATUS_DECLINED, or Attendees.ATTENDEE_STATUS_TENTATIVE. + * To signal the event is an all-day event, "or" ALL_DAY_MASK with the response. + * Alternatively, use buildViewExtraLong(), getResponse(), and isAllDay(). + * + * + * For EventType.GO_TO: + * Set to [.EXTRA_GOTO_TIME] to go to the specified date/time. + * Set to [.EXTRA_GOTO_DATE] to consider the date but ignore the time. + * Set to [.EXTRA_GOTO_BACK_TO_PREVIOUS] if back should bring back previous view. + * Set to [.EXTRA_GOTO_TODAY] if this is a user request to go to the current time. + * + * + * For EventType.UPDATE_TITLE: + * Set formatting flags for Utils.formatDateRange + */ + @JvmField var extraLong: Long = 0 + val isAllDay: Boolean + get() { + if (eventType != EventType.VIEW_EVENT) { + Log.wtf(TAG, "illegal call to isAllDay , wrong event type $eventType") + return false + } + return if (extraLong and ALL_DAY_MASK != 0L) true else false + } + val response: Int + get() { + if (eventType != EventType.VIEW_EVENT) { + Log.wtf(TAG, "illegal call to getResponse , wrong event type $eventType") + return Attendees.ATTENDEE_STATUS_NONE + } + val response = (extraLong and ATTENTEE_STATUS_MASK).toInt() + when (response) { + ATTENDEE_STATUS_NONE_MASK -> return Attendees.ATTENDEE_STATUS_NONE + ATTENDEE_STATUS_ACCEPTED_MASK -> return Attendees.ATTENDEE_STATUS_ACCEPTED + ATTENDEE_STATUS_DECLINED_MASK -> return Attendees.ATTENDEE_STATUS_DECLINED + ATTENDEE_STATUS_TENTATIVE_MASK -> return Attendees.ATTENDEE_STATUS_TENTATIVE + else -> Log.wtf(TAG, "Unknown attendee response $response") + } + return ATTENDEE_STATUS_NONE_MASK + } + + companion object { + private const val ATTENTEE_STATUS_MASK: Long = 0xFF + private const val ALL_DAY_MASK: Long = 0x100 + private const val ATTENDEE_STATUS_NONE_MASK = 0x01 + private const val ATTENDEE_STATUS_ACCEPTED_MASK = 0x02 + private const val ATTENDEE_STATUS_DECLINED_MASK = 0x04 + private const val ATTENDEE_STATUS_TENTATIVE_MASK = 0x08 + + // Used to build the extra long for a VIEW event. + @JvmStatic fun buildViewExtraLong(response: Int, allDay: Boolean): Long { + var extra = if (allDay) ALL_DAY_MASK else 0 + extra = when (response) { + Attendees.ATTENDEE_STATUS_NONE -> extra or + ATTENDEE_STATUS_NONE_MASK.toLong() + Attendees.ATTENDEE_STATUS_ACCEPTED -> extra or + ATTENDEE_STATUS_ACCEPTED_MASK.toLong() + Attendees.ATTENDEE_STATUS_DECLINED -> extra or + ATTENDEE_STATUS_DECLINED_MASK.toLong() + Attendees.ATTENDEE_STATUS_TENTATIVE -> extra or + ATTENDEE_STATUS_TENTATIVE_MASK.toLong() + else -> { + Log.wtf( + TAG, + "Unknown attendee response $response" + ) + extra or ATTENDEE_STATUS_NONE_MASK.toLong() + } + } + return extra + } + } + } + + interface EventHandler { + val supportedEventTypes: Long + fun handleEvent(event: EventInfo?) + + /** + * This notifies the handler that the database has changed and it should + * update its view. + */ + fun eventsChanged() + } + + fun sendEventRelatedEvent( + sender: Object?, + eventType: Long, + eventId: Long, + startMillis: Long, + endMillis: Long, + x: Int, + y: Int, + selectedMillis: Long + ) { + // TODO: pass the real allDay status or at least a status that says we don't know the + // status and have the receiver query the data. + // The current use of this method for VIEW_EVENT is by the day view to show an EventInfo + // so currently the missing allDay status has no effect. + sendEventRelatedEventWithExtra( + sender, eventType, eventId, startMillis, endMillis, x, y, + EventInfo.buildViewExtraLong(Attendees.ATTENDEE_STATUS_NONE, false), + selectedMillis + ) + } + + /** + * Helper for sending New/View/Edit/Delete events + * + * @param sender object of the caller + * @param eventType one of [EventType] + * @param eventId event id + * @param startMillis start time + * @param endMillis end time + * @param x x coordinate in the activity space + * @param y y coordinate in the activity space + * @param extraLong default response value for the "simple event view" and all day indication. + * Use Attendees.ATTENDEE_STATUS_NONE for no response. + * @param selectedMillis The time to specify as selected + */ + fun sendEventRelatedEventWithExtra( + sender: Object?, + eventType: Long, + eventId: Long, + startMillis: Long, + endMillis: Long, + x: Int, + y: Int, + extraLong: Long, + selectedMillis: Long + ) { + sendEventRelatedEventWithExtraWithTitleWithCalendarId( + sender, eventType, eventId, + startMillis, endMillis, x, y, extraLong, selectedMillis, null, -1 + ) + } + + /** + * Helper for sending New/View/Edit/Delete events + * + * @param sender object of the caller + * @param eventType one of [EventType] + * @param eventId event id + * @param startMillis start time + * @param endMillis end time + * @param x x coordinate in the activity space + * @param y y coordinate in the activity space + * @param extraLong default response value for the "simple event view" and all day indication. + * Use Attendees.ATTENDEE_STATUS_NONE for no response. + * @param selectedMillis The time to specify as selected + * @param title The title of the event + * @param calendarId The id of the calendar which the event belongs to + */ + fun sendEventRelatedEventWithExtraWithTitleWithCalendarId( + sender: Object?, + eventType: Long, + eventId: Long, + startMillis: Long, + endMillis: Long, + x: Int, + y: Int, + extraLong: Long, + selectedMillis: Long, + title: String?, + calendarId: Long + ) { + val info = EventInfo() + info.eventType = eventType + if (eventType == EventType.VIEW_EVENT_DETAILS) { + info.viewType = ViewType.CURRENT + } + info.id = eventId + info.startTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone)) + (info.startTime as Time).set(startMillis) + if (selectedMillis != -1L) { + info.selectedTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone)) + (info.selectedTime as Time).set(selectedMillis) + } else { + info.selectedTime = info.startTime + } + info.endTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone)) + (info.endTime as Time).set(endMillis) + info.x = x + info.y = y + info.extraLong = extraLong + info.eventTitle = title + info.calendarId = calendarId + this.sendEvent(sender, info) + } + + /** + * Helper for sending non-calendar-event events + * + * @param sender object of the caller + * @param eventType one of [EventType] + * @param start start time + * @param end end time + * @param eventId event id + * @param viewType [ViewType] + */ + fun sendEvent( + sender: Object?, + eventType: Long, + start: Time?, + end: Time?, + eventId: Long, + viewType: Int + ) { + sendEvent( + sender, eventType, start, end, start, eventId, viewType, EXTRA_GOTO_TIME, null, + null + ) + } + + /** + * sendEvent() variant with extraLong, search query, and search component name. + */ + fun sendEvent( + sender: Object?, + eventType: Long, + start: Time?, + end: Time?, + eventId: Long, + viewType: Int, + extraLong: Long, + query: String?, + componentName: ComponentName? + ) { + sendEvent( + sender, eventType, start, end, start, eventId, viewType, extraLong, query, + componentName + ) + } + + fun sendEvent( + sender: Object?, + eventType: Long, + start: Time?, + end: Time?, + selected: Time?, + eventId: Long, + viewType: Int, + extraLong: Long, + query: String?, + componentName: ComponentName? + ) { + val info = EventInfo() + info.eventType = eventType + info.startTime = start + info.selectedTime = selected + info.endTime = end + info.id = eventId + info.viewType = viewType + info.query = query + info.componentName = componentName + info.extraLong = extraLong + this.sendEvent(sender, info) + } + + fun sendEvent(sender: Object?, event: EventInfo) { + // TODO Throw exception on invalid events + if (DEBUG) { + Log.d(TAG, eventInfoToString(event)) + } + val filteredTypes: Long? = filters.get(sender) + if (filteredTypes != null && filteredTypes.toLong() and event.eventType != 0L) { + // Suppress event per filter + if (DEBUG) { + Log.d(TAG, "Event suppressed") + } + return + } + previousViewType = viewType + + // Fix up view if not specified + if (event.viewType == ViewType.DETAIL) { + event.viewType = mDetailViewType + viewType = mDetailViewType + } else if (event.viewType == ViewType.CURRENT) { + event.viewType = viewType + } else if (event.viewType != ViewType.EDIT) { + viewType = event.viewType + if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY || + Utils.getAllowWeekForDetailView() && event.viewType == ViewType.WEEK) { + mDetailViewType = viewType + } + } + if (DEBUG) { + Log.d(TAG, "vvvvvvvvvvvvvvv") + Log.d( + TAG, + "Start " + if (event.startTime == null) "null" else event.startTime.toString() + ) + Log.d(TAG, "End " + if (event.endTime == null) "null" else event.endTime.toString()) + Log.d( + TAG, + "Select " + if (event.selectedTime == null) "null" + else event.selectedTime.toString() + ) + Log.d(TAG, "mTime " + if (mTime == null) "null" else mTime.toString()) + } + var startMillis: Long = 0 + val temp = event.startTime + if (temp != null) { + startMillis = (event.startTime as Time).toMillis(false) + } + + // Set mTime if selectedTime is set + val temp1 = event.selectedTime + if (temp1 != null && temp1?.toMillis(false) != 0L) { + mTime?.set(event.selectedTime) + } else { + if (startMillis != 0L) { + // selectedTime is not set so set mTime to startTime iff it is not + // within start and end times + val mtimeMillis: Long = mTime?.toMillis(false) as Long + val temp2 = event.endTime + if (mtimeMillis < startMillis || + temp2 != null && mtimeMillis > temp2.toMillis(false)) { + mTime?.set(event.startTime) + } + } + event.selectedTime = mTime + } + // Store the formatting flags if this is an update to the title + if (event.eventType == EventType.UPDATE_TITLE) { + dateFlags = event.extraLong + } + + // Fix up start time if not specified + if (startMillis == 0L) { + event.startTime = mTime + } + if (DEBUG) { + Log.d( + TAG, + "Start " + if (event.startTime == null) "null" else + event.startTime.toString() + ) + Log.d(TAG, "End " + if (event.endTime == null) "null" else + event.endTime.toString()) + Log.d( + TAG, + "Select " + if (event.selectedTime == null) "null" else + event.selectedTime.toString() + ) + Log.d(TAG, "mTime " + if (mTime == null) "null" else mTime.toString()) + Log.d(TAG, "^^^^^^^^^^^^^^^") + } + + // Store the eventId if we're entering edit event + if ((event.eventType and EventType.VIEW_EVENT_DETAILS) != 0L) { + if (event.id > 0) { + eventId = event.id + } else { + eventId = -1 + } + } + var handled = false + synchronized(this) { + mDispatchInProgressCounter++ + if (DEBUG) { + Log.d( + TAG, + "sendEvent: Dispatching to " + eventHandlers.size.toString() + " handlers" + ) + } + // Dispatch to event handler(s) + val temp3 = mFirstEventHandler + if (temp3 != null) { + // Handle the 'first' one before handling the others + val handler: EventHandler? = mFirstEventHandler?.second + if (handler != null && handler.supportedEventTypes and event.eventType != 0L && + !mToBeRemovedEventHandlers.contains(mFirstEventHandler?.first)) { + handler.handleEvent(event) + handled = true + } + } + val handlers: MutableIterator<MutableMap.MutableEntry<Int, + CalendarController.EventHandler>> = eventHandlers.entries.iterator() + while (handlers.hasNext()) { + val entry: MutableMap.MutableEntry<Int, + CalendarController.EventHandler> = handlers.next() + val key: Int = entry.key.toInt() + val temp4 = mFirstEventHandler + if (temp4 != null && key.toInt() == temp4.first.toInt()) { + // If this was the 'first' handler it was already handled + continue + } + val eventHandler: EventHandler = entry.value + if (eventHandler != null && + eventHandler.supportedEventTypes and event.eventType != 0L) { + if (mToBeRemovedEventHandlers.contains(key)) { + continue + } + eventHandler.handleEvent(event) + handled = true + } + } + mDispatchInProgressCounter-- + if (mDispatchInProgressCounter == 0) { + + // Deregister removed handlers + if (mToBeRemovedEventHandlers.size > 0) { + for (zombie in mToBeRemovedEventHandlers) { + eventHandlers.remove(zombie) + val temp5 = mFirstEventHandler + if (temp5 != null && zombie.equals(temp5.first)) { + mFirstEventHandler = null + } + } + mToBeRemovedEventHandlers.clear() + } + // Add new handlers + if (mToBeAddedFirstEventHandler != null) { + mFirstEventHandler = mToBeAddedFirstEventHandler + mToBeAddedFirstEventHandler = null + } + if (mToBeAddedEventHandlers.size > 0) { + for (food in mToBeAddedEventHandlers.entries) { + eventHandlers.put(food.key, food.value) + } + } + } + } + } + + /** + * Adds or updates an event handler. This uses a LinkedHashMap so that we can + * replace fragments based on the view id they are being expanded into. + * + * @param key The view id or placeholder for this handler + * @param eventHandler Typically a fragment or activity in the calendar app + */ + fun registerEventHandler(key: Int, eventHandler: EventHandler?) { + synchronized(this) { + if (mDispatchInProgressCounter > 0) { + mToBeAddedEventHandlers.put(key, + eventHandler as CalendarController.EventHandler) + } else { + eventHandlers.put(key, eventHandler as CalendarController.EventHandler) + } + } + } + + fun registerFirstEventHandler(key: Int, eventHandler: EventHandler?) { + synchronized(this) { + registerEventHandler(key, eventHandler) + if (mDispatchInProgressCounter > 0) { + mToBeAddedFirstEventHandler = Pair<Int, EventHandler>(key, eventHandler) + } else { + mFirstEventHandler = Pair<Int, EventHandler>(key, eventHandler) + } + } + } + + fun deregisterEventHandler(key: Int) { + synchronized(this) { + if (mDispatchInProgressCounter > 0) { + // To avoid ConcurrencyException, stash away the event handler for now. + mToBeRemovedEventHandlers.add(key) + } else { + eventHandlers.remove(key) + val temp6 = mFirstEventHandler + if (temp6 != null && temp6.first == key) { + mFirstEventHandler = null + } else {} + } + } + } + + fun deregisterAllEventHandlers() { + synchronized(this) { + if (mDispatchInProgressCounter > 0) { + // To avoid ConcurrencyException, stash away the event handler for now. + mToBeRemovedEventHandlers.addAll(eventHandlers.keys) + } else { + eventHandlers.clear() + mFirstEventHandler = null + } + } + } + + // FRAG_TODO doesn't work yet + fun filterBroadcasts(sender: Object?, eventTypes: Long) { + filters.put(sender, eventTypes) + } + /** + * @return the time that this controller is currently pointed at + */ + /** + * Set the time this controller is currently pointed at + * + * @param millisTime Time since epoch in millis + */ + var time: Long? + get() = mTime?.toMillis(false) + set(millisTime) { + mTime?.set(millisTime as Long) + } + + fun launchViewEvent(eventId: Long, startMillis: Long, endMillis: Long, response: Int) { + val intent = Intent(Intent.ACTION_VIEW) + val eventUri: Uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId) + intent.setData(eventUri) + intent.setClass(mContext as Context, AllInOneActivity::class.java) + intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis) + intent.putExtra(EXTRA_EVENT_END_TIME, endMillis) + intent.putExtra(ATTENDEE_STATUS, response) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + mContext?.startActivity(intent) + } + + private fun eventInfoToString(eventInfo: EventInfo): String { + var tmp = "Unknown" + val builder = StringBuilder() + if (eventInfo.eventType and EventType.GO_TO != 0L) { + tmp = "Go to time/event" + } else if (eventInfo.eventType and EventType.VIEW_EVENT != 0L) { + tmp = "View event" + } else if (eventInfo.eventType and EventType.VIEW_EVENT_DETAILS != 0L) { + tmp = "View details" + } else if (eventInfo.eventType and EventType.EVENTS_CHANGED != 0L) { + tmp = "Refresh events" + } else if (eventInfo.eventType and EventType.USER_HOME != 0L) { + tmp = "Gone home" + } else if (eventInfo.eventType and EventType.UPDATE_TITLE != 0L) { + tmp = "Update title" + } + builder.append(tmp) + builder.append(": id=") + builder.append(eventInfo.id) + builder.append(", selected=") + builder.append(eventInfo.selectedTime) + builder.append(", start=") + builder.append(eventInfo.startTime) + builder.append(", end=") + builder.append(eventInfo.endTime) + builder.append(", viewType=") + builder.append(eventInfo.viewType) + builder.append(", x=") + builder.append(eventInfo.x) + builder.append(", y=") + builder.append(eventInfo.y) + return builder.toString() + } + + companion object { + private const val DEBUG = false + private const val TAG = "CalendarController" + const val EVENT_EDIT_ON_LAUNCH = "editMode" + const val MIN_CALENDAR_YEAR = 1970 + const val MAX_CALENDAR_YEAR = 2036 + const val MIN_CALENDAR_WEEK = 0 + const val MAX_CALENDAR_WEEK = 3497 // weeks between 1/1/1970 and 1/1/2037 + private val instances: WeakHashMap<Context, WeakReference<CalendarController>> = + WeakHashMap<Context, WeakReference<CalendarController>>() + + /** + * Pass to the ExtraLong parameter for EventType.GO_TO to signal the time + * can be ignored + */ + const val EXTRA_GOTO_DATE: Long = 1 + const val EXTRA_GOTO_TIME: Long = 2 + const val EXTRA_GOTO_BACK_TO_PREVIOUS: Long = 4 + const val EXTRA_GOTO_TODAY: Long = 8 + + /** + * Creates and/or returns an instance of CalendarController associated with + * the supplied context. It is best to pass in the current Activity. + * + * @param context The activity if at all possible. + */ + @JvmStatic fun getInstance(context: Context?): CalendarController? { + synchronized(instances) { + var controller: CalendarController? = null + val weakController: WeakReference<CalendarController>? = instances.get(context) + if (weakController != null) { + controller = weakController.get() + } + if (controller == null) { + controller = CalendarController(context) + instances.put(context, WeakReference(controller)) + } + return controller + } + } + + /** + * Removes an instance when it is no longer needed. This should be called in + * an activity's onDestroy method. + * + * @param context The activity used to create the controller + */ + @JvmStatic fun removeInstance(context: Context?) { + instances.remove(context) + } + } + + init { + mContext = context + mUpdateTimezone.run() + mTime?.setToNow() + mDetailViewType = Utils.getSharedPreference( + mContext, + GeneralPreferences.KEY_DETAILED_VIEW, + GeneralPreferences.DEFAULT_DETAILED_VIEW + ) + } +}
\ No newline at end of file |