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 }