blob: 16ee8fdd5ce8cedd4824b56c552672bf757fd778 [file] [log] [blame]
/*
* 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
)
}
}