| /* |
| * 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() |
| } |
| } |