1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.calendar 17 18 import android.content.ContentResolver 19 import android.content.Context 20 import android.database.Cursor 21 import android.os.Handler 22 import android.os.Process 23 import android.provider.CalendarContract 24 import android.provider.CalendarContract.EventDays 25 import android.util.Log 26 import java.util.ArrayList 27 import java.util.Arrays 28 import java.util.concurrent.LinkedBlockingQueue 29 import java.util.concurrent.atomic.AtomicInteger 30 31 class EventLoader(context: Context) { 32 private val mContext: Context 33 private val mHandler: Handler = Handler() 34 private val mSequenceNumber: AtomicInteger? = AtomicInteger() 35 private val mLoaderQueue: LinkedBlockingQueue<LoadRequest> 36 private var mLoaderThread: LoaderThread? = null 37 private val mResolver: ContentResolver 38 39 private interface LoadRequest { processRequestnull40 fun processRequest(eventLoader: EventLoader?) 41 fun skipRequest(eventLoader: EventLoader?) 42 } 43 44 private class ShutdownRequest : LoadRequest { 45 override fun processRequest(eventLoader: EventLoader?) {} 46 override fun skipRequest(eventLoader: EventLoader?) {} 47 } 48 49 /** 50 * 51 * Code for handling requests to get whether days have an event or not 52 * and filling in the eventDays array. 53 * 54 */ 55 private class LoadEventDaysRequest( 56 var startDay: Int, 57 var numDays: Int, 58 var eventDays: BooleanArray, 59 uiCallback: Runnable 60 ) : LoadRequest { 61 var uiCallback: Runnable 62 @Override processRequestnull63 override fun processRequest(eventLoader: EventLoader?) { 64 val handler: Handler? = eventLoader?.mHandler 65 val cr: ContentResolver? = eventLoader?.mResolver 66 67 // Clear the event days 68 Arrays.fill(eventDays, false) 69 70 // query which days have events 71 val cursor: Cursor = EventDays.query(cr, startDay, numDays, PROJECTION) 72 try { 73 val startDayColumnIndex: Int = cursor.getColumnIndexOrThrow(EventDays.STARTDAY) 74 val endDayColumnIndex: Int = cursor.getColumnIndexOrThrow(EventDays.ENDDAY) 75 76 // Set all the days with events to true 77 while (cursor.moveToNext()) { 78 val firstDay: Int = cursor.getInt(startDayColumnIndex) 79 val lastDay: Int = cursor.getInt(endDayColumnIndex) 80 // we want the entire range the event occurs, but only within the month 81 val firstIndex: Int = Math.max(firstDay - startDay, 0) 82 val lastIndex: Int = Math.min(lastDay - startDay, 30) 83 for (i in firstIndex..lastIndex) { 84 eventDays[i] = true 85 } 86 } 87 } finally { 88 if (cursor != null) { 89 cursor.close() 90 } 91 } 92 handler?.post(uiCallback) 93 } 94 95 @Override skipRequestnull96 override fun skipRequest(eventLoader: EventLoader?) { 97 } 98 99 companion object { 100 /** 101 * The projection used by the EventDays query. 102 */ 103 private val PROJECTION = arrayOf<String>( 104 CalendarContract.EventDays.STARTDAY, CalendarContract.EventDays.ENDDAY 105 ) 106 } 107 108 init { 109 this.uiCallback = uiCallback 110 } 111 } 112 113 private class LoadEventsRequest( 114 var id: Int, 115 var startDay: Int, 116 var numDays: Int, 117 events: ArrayList<Event?>, 118 successCallback: Runnable, 119 cancelCallback: Runnable 120 ) : LoadRequest { 121 var events: ArrayList<Event?> 122 var successCallback: Runnable 123 var cancelCallback: Runnable 124 @Override processRequestnull125 override fun processRequest(eventLoader: EventLoader?) { 126 Event.loadEvents(eventLoader?.mContext, events, startDay, 127 numDays, id, eventLoader?.mSequenceNumber) 128 129 // Check if we are still the most recent request. 130 if (id == eventLoader?.mSequenceNumber?.get()) { 131 eventLoader.mHandler.post(successCallback) 132 } else { 133 eventLoader?.mHandler?.post(cancelCallback) 134 } 135 } 136 137 @Override skipRequestnull138 override fun skipRequest(eventLoader: EventLoader?) { 139 eventLoader?.mHandler?.post(cancelCallback) 140 } 141 142 init { 143 this.events = events 144 this.successCallback = successCallback 145 this.cancelCallback = cancelCallback 146 } 147 } 148 149 private class LoaderThread( 150 queue: LinkedBlockingQueue<LoadRequest>, 151 eventLoader: EventLoader 152 ) : Thread() { 153 var mQueue: LinkedBlockingQueue<LoadRequest> 154 var mEventLoader: EventLoader shutdownnull155 fun shutdown() { 156 try { 157 mQueue.put(ShutdownRequest()) 158 } catch (ex: InterruptedException) { 159 // The put() method fails with InterruptedException if the 160 // queue is full. This should never happen because the queue 161 // has no limit. 162 Log.e("Cal", "LoaderThread.shutdown() interrupted!") 163 } 164 } 165 166 @Override runnull167 override fun run() { 168 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) 169 while (true) { 170 try { 171 // Wait for the next request 172 var request: LoadRequest = mQueue.take() 173 174 // If there are a bunch of requests already waiting, then 175 // skip all but the most recent request. 176 while (!mQueue.isEmpty()) { 177 // Let the request know that it was skipped 178 request.skipRequest(mEventLoader) 179 180 // Skip to the next request 181 request = mQueue.take() 182 } 183 if (request is ShutdownRequest) { 184 return 185 } 186 request.processRequest(mEventLoader) 187 } catch (ex: InterruptedException) { 188 Log.e("Cal", "background LoaderThread interrupted!") 189 } 190 } 191 } 192 193 init { 194 mQueue = queue 195 mEventLoader = eventLoader 196 } 197 } 198 199 /** 200 * Call this from the activity's onResume() 201 */ startBackgroundThreadnull202 fun startBackgroundThread() { 203 mLoaderThread = LoaderThread(mLoaderQueue, this) 204 mLoaderThread?.start() 205 } 206 207 /** 208 * Call this from the activity's onPause() 209 */ stopBackgroundThreadnull210 fun stopBackgroundThread() { 211 mLoaderThread!!.shutdown() 212 } 213 214 /** 215 * Loads "numDays" days worth of events, starting at start, into events. 216 * Posts uiCallback to the [Handler] for this view, which will run in the UI thread. 217 * Reuses an existing background thread, if events were already being loaded in the background. 218 * NOTE: events and uiCallback are not used if an existing background thread gets reused -- 219 * the ones that were passed in on the call that results in the background thread getting 220 * created are used, and the most recent call's worth of data is loaded into events and posted 221 * via the uiCallback. 222 */ loadEventsInBackgroundnull223 fun loadEventsInBackground( 224 numDays: Int, 225 events: ArrayList<Event?>, 226 startDay: Int, 227 successCallback: Runnable, 228 cancelCallback: Runnable 229 ) { 230 231 // Increment the sequence number for requests. We don't care if the 232 // sequence numbers wrap around because we test for equality with the 233 // latest one. 234 val id: Int = mSequenceNumber?.incrementAndGet() as Int 235 236 // Send the load request to the background thread 237 val request = LoadEventsRequest(id, startDay, numDays, 238 events, successCallback, cancelCallback) 239 try { 240 mLoaderQueue.put(request) 241 } catch (ex: InterruptedException) { 242 // The put() method fails with InterruptedException if the 243 // queue is full. This should never happen because the queue 244 // has no limit. 245 Log.e("Cal", "loadEventsInBackground() interrupted!") 246 } 247 } 248 249 /** 250 * Sends a request for the days with events to be marked. Loads "numDays" 251 * worth of days, starting at start, and fills in eventDays to express which 252 * days have events. 253 * 254 * @param startDay First day to check for events 255 * @param numDays Days following the start day to check 256 * @param eventDay Whether or not an event exists on that day 257 * @param uiCallback What to do when done (log data, redraw screen) 258 */ loadEventDaysInBackgroundnull259 fun loadEventDaysInBackground( 260 startDay: Int, 261 numDays: Int, 262 eventDays: BooleanArray, 263 uiCallback: Runnable 264 ) { 265 // Send load request to the background thread 266 val request = LoadEventDaysRequest(startDay, numDays, 267 eventDays, uiCallback) 268 try { 269 mLoaderQueue.put(request) 270 } catch (ex: InterruptedException) { 271 // The put() method fails with InterruptedException if the 272 // queue is full. This should never happen because the queue 273 // has no limit. 274 Log.e("Cal", "loadEventDaysInBackground() interrupted!") 275 } 276 } 277 278 init { 279 mContext = context 280 mLoaderQueue = LinkedBlockingQueue<LoadRequest>() 281 mResolver = context.getContentResolver() 282 } 283 }