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