/* * 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 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( CalendarContract.EventDays.STARTDAY, CalendarContract.EventDays.ENDDAY ) } init { this.uiCallback = uiCallback } } private class LoadEventsRequest( var id: Int, var startDay: Int, var numDays: Int, events: ArrayList, successCallback: Runnable, cancelCallback: Runnable ) : LoadRequest { var events: ArrayList 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, eventLoader: EventLoader ) : Thread() { var mQueue: LinkedBlockingQueue 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, 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() mResolver = context.getContentResolver() } }