blob: a05e8a2e2dab6b7749e25b0eac797cd5c38d2653 [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.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.os.Handler
import android.os.Process
import android.provider.CalendarContract
import android.provider.CalendarContract.EventDays
import android.util.Log
import java.util.ArrayList
import java.util.Arrays
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicInteger
class EventLoader(context: Context) {
private val mContext: Context
private val mHandler: Handler = Handler()
private val mSequenceNumber: AtomicInteger? = AtomicInteger()
private val mLoaderQueue: LinkedBlockingQueue<LoadRequest>
private var mLoaderThread: LoaderThread? = null
private val mResolver: ContentResolver
private interface LoadRequest {
fun processRequest(eventLoader: EventLoader?)
fun skipRequest(eventLoader: EventLoader?)
}
private class ShutdownRequest : LoadRequest {
override fun processRequest(eventLoader: EventLoader?) {}
override fun skipRequest(eventLoader: EventLoader?) {}
}
/**
*
* Code for handling requests to get whether days have an event or not
* and filling in the eventDays array.
*
*/
private class LoadEventDaysRequest(
var startDay: Int,
var numDays: Int,
var eventDays: BooleanArray,
uiCallback: Runnable
) : LoadRequest {
var uiCallback: Runnable
@Override
override fun processRequest(eventLoader: EventLoader?) {
val handler: Handler? = eventLoader?.mHandler
val cr: ContentResolver? = eventLoader?.mResolver
// Clear the event days
Arrays.fill(eventDays, false)
// query which days have events
val cursor: Cursor = EventDays.query(cr, startDay, numDays, PROJECTION)
try {
val startDayColumnIndex: Int = cursor.getColumnIndexOrThrow(EventDays.STARTDAY)
val endDayColumnIndex: Int = cursor.getColumnIndexOrThrow(EventDays.ENDDAY)
// Set all the days with events to true
while (cursor.moveToNext()) {
val firstDay: Int = cursor.getInt(startDayColumnIndex)
val lastDay: Int = cursor.getInt(endDayColumnIndex)
// we want the entire range the event occurs, but only within the month
val firstIndex: Int = Math.max(firstDay - startDay, 0)
val lastIndex: Int = Math.min(lastDay - startDay, 30)
for (i in firstIndex..lastIndex) {
eventDays[i] = true
}
}
} finally {
if (cursor != null) {
cursor.close()
}
}
handler?.post(uiCallback)
}
@Override
override fun skipRequest(eventLoader: EventLoader?) {
}
companion object {
/**
* The projection used by the EventDays query.
*/
private val PROJECTION = arrayOf<String>(
CalendarContract.EventDays.STARTDAY, CalendarContract.EventDays.ENDDAY
)
}
init {
this.uiCallback = uiCallback
}
}
private class LoadEventsRequest(
var id: Int,
var startDay: Int,
var numDays: Int,
events: ArrayList<Event?>,
successCallback: Runnable,
cancelCallback: Runnable
) : LoadRequest {
var events: ArrayList<Event?>
var successCallback: Runnable
var cancelCallback: Runnable
@Override
override fun processRequest(eventLoader: EventLoader?) {
Event.loadEvents(eventLoader?.mContext, events, startDay,
numDays, id, eventLoader?.mSequenceNumber)
// Check if we are still the most recent request.
if (id == eventLoader?.mSequenceNumber?.get()) {
eventLoader?.mHandler?.post(successCallback)
} else {
eventLoader?.mHandler?.post(cancelCallback)
}
}
@Override
override fun skipRequest(eventLoader: EventLoader?) {
eventLoader?.mHandler?.post(cancelCallback)
}
init {
this.events = events
this.successCallback = successCallback
this.cancelCallback = cancelCallback
}
}
private class LoaderThread(
queue: LinkedBlockingQueue<LoadRequest>,
eventLoader: EventLoader
) : Thread() {
var mQueue: LinkedBlockingQueue<LoadRequest>
var mEventLoader: EventLoader
fun shutdown() {
try {
mQueue.put(ShutdownRequest())
} catch (ex: InterruptedException) {
// The put() method fails with InterruptedException if the
// queue is full. This should never happen because the queue
// has no limit.
Log.e("Cal", "LoaderThread.shutdown() interrupted!")
}
}
@Override
override fun run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
while (true) {
try {
// Wait for the next request
var request: LoadRequest = mQueue.take()
// If there are a bunch of requests already waiting, then
// skip all but the most recent request.
while (!mQueue.isEmpty()) {
// Let the request know that it was skipped
request.skipRequest(mEventLoader)
// Skip to the next request
request = mQueue.take()
}
if (request is ShutdownRequest) {
return
}
request.processRequest(mEventLoader)
} catch (ex: InterruptedException) {
Log.e("Cal", "background LoaderThread interrupted!")
}
}
}
init {
mQueue = queue
mEventLoader = eventLoader
}
}
/**
* Call this from the activity's onResume()
*/
fun startBackgroundThread() {
mLoaderThread = LoaderThread(mLoaderQueue, this)
mLoaderThread?.start()
}
/**
* Call this from the activity's onPause()
*/
fun stopBackgroundThread() {
mLoaderThread!!.shutdown()
}
/**
* Loads "numDays" days worth of events, starting at start, into events.
* Posts uiCallback to the [Handler] for this view, which will run in the UI thread.
* Reuses an existing background thread, if events were already being loaded in the background.
* NOTE: events and uiCallback are not used if an existing background thread gets reused --
* the ones that were passed in on the call that results in the background thread getting
* created are used, and the most recent call's worth of data is loaded into events and posted
* via the uiCallback.
*/
fun loadEventsInBackground(
numDays: Int,
events: ArrayList<Event?>,
startDay: Int,
successCallback: Runnable,
cancelCallback: Runnable
) {
// Increment the sequence number for requests. We don't care if the
// sequence numbers wrap around because we test for equality with the
// latest one.
val id: Int = mSequenceNumber?.incrementAndGet() as Int
// Send the load request to the background thread
val request = LoadEventsRequest(id, startDay, numDays,
events, successCallback, cancelCallback)
try {
mLoaderQueue.put(request)
} catch (ex: InterruptedException) {
// The put() method fails with InterruptedException if the
// queue is full. This should never happen because the queue
// has no limit.
Log.e("Cal", "loadEventsInBackground() interrupted!")
}
}
/**
* Sends a request for the days with events to be marked. Loads "numDays"
* worth of days, starting at start, and fills in eventDays to express which
* days have events.
*
* @param startDay First day to check for events
* @param numDays Days following the start day to check
* @param eventDay Whether or not an event exists on that day
* @param uiCallback What to do when done (log data, redraw screen)
*/
fun loadEventDaysInBackground(
startDay: Int,
numDays: Int,
eventDays: BooleanArray,
uiCallback: Runnable
) {
// Send load request to the background thread
val request = LoadEventDaysRequest(startDay, numDays,
eventDays, uiCallback)
try {
mLoaderQueue.put(request)
} catch (ex: InterruptedException) {
// The put() method fails with InterruptedException if the
// queue is full. This should never happen because the queue
// has no limit.
Log.e("Cal", "loadEventDaysInBackground() interrupted!")
}
}
init {
mContext = context
mLoaderQueue = LinkedBlockingQueue<LoadRequest>()
mResolver = context.getContentResolver()
}
}