2 * Copyright (C) 2011 Morphoss Ltd
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package com
.morphoss
.acal
.dataservice
;
22 import java
.io
.FileInputStream
;
23 import java
.io
.FileNotFoundException
;
24 import java
.io
.FileOutputStream
;
25 import java
.io
.IOException
;
26 import java
.io
.ObjectInputStream
;
27 import java
.io
.ObjectOutputStream
;
28 import java
.util
.ArrayList
;
29 import java
.util
.Collections
;
30 import java
.util
.HashMap
;
31 import java
.util
.List
;
33 import java
.util
.PriorityQueue
;
34 import java
.util
.Queue
;
36 import java
.util
.Map
.Entry
;
37 import java
.util
.concurrent
.ConcurrentHashMap
;
38 import java
.util
.concurrent
.ConcurrentLinkedQueue
;
40 import android
.app
.AlarmManager
;
41 import android
.app
.PendingIntent
;
42 import android
.app
.Service
;
43 import android
.content
.ContentValues
;
44 import android
.content
.Context
;
45 import android
.content
.Intent
;
46 import android
.content
.SharedPreferences
;
47 import android
.database
.Cursor
;
48 import android
.database
.DatabaseUtils
;
49 import android
.net
.Uri
;
50 import android
.os
.ConditionVariable
;
51 import android
.os
.IBinder
;
52 import android
.os
.RemoteException
;
53 import android
.preference
.PreferenceManager
;
54 import android
.util
.Log
;
55 import android
.widget
.Toast
;
57 import com
.morphoss
.acal
.AcalDebug
;
58 import com
.morphoss
.acal
.Constants
;
59 import com
.morphoss
.acal
.DatabaseChangedEvent
;
60 import com
.morphoss
.acal
.DatabaseEventListener
;
61 import com
.morphoss
.acal
.R
;
62 import com
.morphoss
.acal
.acaltime
.AcalDateRange
;
63 import com
.morphoss
.acal
.acaltime
.AcalDateTime
;
64 import com
.morphoss
.acal
.acaltime
.AcalDuration
;
65 import com
.morphoss
.acal
.activity
.AlarmActivity
;
66 import com
.morphoss
.acal
.activity
.TodoEdit
;
67 import com
.morphoss
.acal
.davacal
.AcalAlarm
;
68 import com
.morphoss
.acal
.davacal
.AcalCollection
;
69 import com
.morphoss
.acal
.davacal
.AcalEvent
;
70 import com
.morphoss
.acal
.davacal
.Masterable
;
71 import com
.morphoss
.acal
.davacal
.PropertyName
;
72 import com
.morphoss
.acal
.davacal
.SimpleAcalEvent
;
73 import com
.morphoss
.acal
.davacal
.SimpleAcalTodo
;
74 import com
.morphoss
.acal
.davacal
.VCalendar
;
75 import com
.morphoss
.acal
.davacal
.VComponent
;
76 import com
.morphoss
.acal
.davacal
.VComponentCreationException
;
77 import com
.morphoss
.acal
.davacal
.VTodo
;
78 import com
.morphoss
.acal
.davacal
.YouMustSurroundThisMethodInTryCatchOrIllEatYouException
;
79 import com
.morphoss
.acal
.desktop
.ShowUpcomingWidgetProvider
;
80 import com
.morphoss
.acal
.providers
.DavCollections
;
81 import com
.morphoss
.acal
.providers
.DavResources
;
82 import com
.morphoss
.acal
.providers
.PendingChanges
;
83 import com
.morphoss
.acal
.providers
.Servers
;
84 import com
.morphoss
.acal
.service
.ServiceJob
;
85 import com
.morphoss
.acal
.service
.SyncChangesToServer
;
86 import com
.morphoss
.acal
.service
.WorkerClass
;
87 import com
.morphoss
.acal
.service
.aCalService
;
91 * The CalendarDataService provides an interface between calendar data in the database and AcalDateTime
92 * Objects used by aCal activities. The service should be started with aCalService, and run at all times to
93 * maximise activity response.
96 * The Service keeps a memory model of the state of all Resources in the database. It is also responsible for
97 * transmitting user changes to an updater class.
100 * It is important that this model of the calendar matches exactly that of the database at all times. As such
101 * the service listens to DatabaseChanged events. The service places an invariant on the rest of the
102 * application -> Any class that changes DavResources MUST Notify this service by dispatching a
103 * DatabaseChanged Event.
106 * The provides an interface to any activity that wishes to use it via DataRequest.aidl. Activities can also
107 * register a call back using DataRequestCallBack.aidl, if they wish to be notified of state changes.
110 * @author Morphoss Ltd
113 public class CalendarDataService
extends Service
implements Runnable
, DatabaseEventListener
{
115 public static final String TAG
= "aCal CalendarDataService";
116 /** The file that we save state information to */
117 public static final String STATE_FILE
= "/data/data/com.morphoss.acal/cds.dat";
118 private Queue
<ContentValues
> resourcesPending
= new ConcurrentLinkedQueue
<ContentValues
>(); //requests to add resources
119 private Map
<Integer
,VCalendar
> calendars
= new ConcurrentHashMap
<Integer
,VCalendar
>();
120 private Map
<Integer
,AcalCollection
> collections
= new ConcurrentHashMap
<Integer
,AcalCollection
>(); //Keeps the state of collection information
121 private Map
<Integer
,VCalendar
> newResources
= new ConcurrentHashMap
<Integer
,VCalendar
>(); //i=prid resources that have been added but not written to server
123 private ConditionVariable threadHolder
= new ConditionVariable(true);
124 public volatile boolean interrupted
= false; //interrupt control
125 public Thread worker
;
127 private DataRequest
.Stub dataRequest
= new DataRequestHandler();
129 private DataRequestCallBack callback
= null;
131 private static long lastSetEarlyStamp
= 0;
132 private static AcalDateTime earlyTimeStamp
= null;
134 private boolean intialise
= false; //State information
135 private boolean processingNewData
= false;
136 public static final int UPDATE
= 1;
138 public static final String BIND_KEY
= "BIND_MODE";
139 public static final int BIND_DATA_REQUEST
= 0;
140 public static final int BIND_ALARM_TRIGGER
= 1;
143 private static final AcalDateTime MIN_ALARM_AGE
= AcalDateTime
.addDuration(new AcalDateTime(), new AcalDuration("-PT24H")); // now -1 Day
145 private PriorityQueue
<AcalAlarm
> alarmQueue
= new PriorityQueue
<AcalAlarm
>();
146 private PriorityQueue
<AcalAlarm
> snoozeQueue
= new PriorityQueue
<AcalAlarm
>();
147 private AcalDateTime lastTriggeredAlarmTime
= MIN_ALARM_AGE
; //default start
148 private PendingIntent alarmIntent
= null;
149 private AlarmManager alarmManager
;
150 private long nextTriggerTime
= Long
.MAX_VALUE
;
152 private long inResourceTx
= 0;
153 private boolean changesDuringTx
= false;
154 final private static long MAX_TX_AGE
= 30000;
156 /*****************************************
157 * Life-cycle overrides *
158 *****************************************/
161 public void onCreate() {
163 earlyTimeStamp
= new AcalDateTime().addDays(35); // Set it some time in a future month
164 updateEarlyTimeStamp(new AcalDateTime()); // Now rationalise it back earlier
166 //immediately start listening for changes to the database
167 aCalService
.databaseDispatcher
.addListener(this);
169 //Set up alarm manager for alarms
170 alarmManager
= (AlarmManager
) getSystemService(Context
.ALARM_SERVICE
);
175 //Get our worker thread ready for action
176 this.threadHolder
.close();
177 if (worker
== null) {
178 worker
= new Thread(this);
184 public IBinder
onBind(Intent arg0
) {
188 public DataRequest
.Stub
getDataRequest() {
189 return this.dataRequest
;
193 public void onDestroy() {
196 //Stop the worker thread
197 this.interrupted
= true;
198 if (worker
!= null) worker
.interrupt();
202 @SuppressWarnings("unchecked")
203 public void loadState() {
205 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Loading calendar data service state from file.");
206 ObjectInputStream inputStream
= null;
207 Object lat
= null; //last alarm trigger time
210 File f
= new File(STATE_FILE
);
212 //File does not exist.
213 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "No state file to load.");
216 inputStream
= new ObjectInputStream(new FileInputStream(STATE_FILE
));
217 lat
= inputStream
.readObject();
218 sq
= inputStream
.readObject();
219 } catch (ClassNotFoundException ex
) {
220 Log
.w(TAG
,"Error loading CDS State - Incomplete data: "+ex
.getMessage());
221 } catch (FileNotFoundException ex
) {
222 if (Constants
.LOG_DEBUG
)Log
.d(TAG
,"Error loading CDS State - File Not Found: "+ex
.getMessage());
223 } catch (IOException ex
) {
224 Log
.w(TAG
,"Error loading CDS State - IO Error: "+ex
.getMessage());
226 //Close the ObjectOutputStream
228 if (inputStream
!= null)
230 } catch (IOException ex
) {
231 Log
.w(TAG
,"Error closing CDS file - IO Error: "+ex
.getMessage());
234 if (lat
!= null && lat
instanceof AcalDateTime
) {
235 lastTriggeredAlarmTime
= (AcalDateTime
)lat
;
236 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
)
237 Log
.d(TAG
,"Read last triggered alarm time as "+lastTriggeredAlarmTime
);
239 if (sq
!= null && sq
instanceof PriorityQueue
<?
>) {
240 this.snoozeQueue
= (PriorityQueue
<AcalAlarm
>) sq
;
241 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
)
242 Log
.d(TAG
,"Loaded snooze queue with "+snoozeQueue
.size()+" elements.");
247 public void saveState() {
249 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Writing cds state to file. Last Triggered Time: "+lastTriggeredAlarmTime
);
250 ObjectOutputStream outputStream
= null;
252 outputStream
= new ObjectOutputStream(new FileOutputStream(STATE_FILE
));
253 outputStream
.writeObject(this.lastTriggeredAlarmTime
);
254 outputStream
.writeObject(this.snoozeQueue
);
255 } catch (FileNotFoundException ex
) {
256 Log
.w(TAG
,"Error saving cds State - File Not Found: "+ex
.getMessage());
257 Log
.w(TAG
,Log
.getStackTraceString(ex
));
258 } catch (IOException ex
) {
259 Log
.w(TAG
,"Error saving cds State - IO Error: "+ex
.getMessage());
260 Log
.w(TAG
,Log
.getStackTraceString(ex
));
261 } catch (Exception ex
) {
262 Log
.w(TAG
,"Error saving cds State: "+ex
.getMessage());
263 Log
.w(TAG
,Log
.getStackTraceString(ex
));
265 //Close the ObjectOutputStream
267 if (outputStream
!= null) {
268 outputStream
.flush();
269 outputStream
.close();
271 } catch (IOException ex
) {
272 Log
.w(TAG
,"Error closing cds file - IO Error: "+ex
.getMessage());
273 Log
.w(TAG
,Log
.getStackTraceString(ex
));
281 * Update the earlyTimeStamp field we use so as not to retain ancient events in memory. We do this with
282 * quite a lot of granularity so it doesn't happen often.
286 public void updateEarlyTimeStamp(AcalDateTime earliestVisible
) {
287 if ( earliestVisible
== null ) return;
288 AcalDateTime latestEarlyTimeStamp
= new AcalDateTime().applyLocalTimeZone().addMonths(-1).setDaySecond(0);
289 latestEarlyTimeStamp
.setMonthDay(1);
290 latestEarlyTimeStamp
.addDays(-5);
292 if ( earliestVisible
.after(earlyTimeStamp
) ) return;
294 AcalDateTime wantEarlyStamp
= earliestVisible
.clone().addDays(-5);
295 wantEarlyStamp
.setMonthDay(1);
296 wantEarlyStamp
.addDays(-5);
297 if ( wantEarlyStamp
.after(latestEarlyTimeStamp
) )
298 wantEarlyStamp
= latestEarlyTimeStamp
;
300 int daysDiff
= earlyTimeStamp
.getDurationTo(wantEarlyStamp
).getDays();
302 long now
= System
.currentTimeMillis();
303 if ( daysDiff
> 0 && lastSetEarlyStamp
> (now
- 30000L) ) return;
304 lastSetEarlyStamp
= now
;
306 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) Log
.println(Constants
.LOGD
, TAG
,
307 "Considering setting early timestamp to "+wantEarlyStamp
.fmtIcal() );
308 if ( daysDiff
< 0 || daysDiff
> 10 ) {
309 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) Log
.println(Constants
.LOGD
, TAG
,
310 "Setting early timestamp to "+wantEarlyStamp
.fmtIcal() );
312 AcalDateTime oldEarlyStamp
= earlyTimeStamp
;
313 earlyTimeStamp
= wantEarlyStamp
;
314 if ( daysDiff
< -10 )
315 fetchOldResources(oldEarlyStamp
);
317 discardOldResources();
325 * This method checks the resourcesPending queue and processes any new resources.
327 * @return true If we did something that might
329 private boolean processPendingResources() {
330 if (Constants
.LOG_DEBUG
) Log
.i(TAG
, "Processing pending resources...");
332 //If no resources to compute, update state and return.
333 if (resourcesPending
.isEmpty()) {
339 long begin
= System
.currentTimeMillis();
340 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) Log
.println(Constants
.LOGD
,TAG
,
341 "Processing resources queue with "+resourcesPending
.size()+" elements.");
342 while (!resourcesPending
.isEmpty()) {
343 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Processing a pending resource");
346 //Remove all line wrapping prior to parsing
347 //create calendar object
349 ContentValues resource
= resourcesPending
.poll();
350 int collectionId
= resource
.getAsInteger(DavResources
.COLLECTION_ID
);
351 if ( resource
.getAsInteger(DavResources
._ID
) == null ) {
352 if (Constants
.LOG_DEBUG
) Log
.d(TAG
,"Null ResourceID: "+resource
.getAsString(DavResources
.RESOURCE_NAME
));
353 if (Constants
.LOG_DEBUG
) Log
.d(TAG
,"Null ResourceID: "+resource
.getAsString(DavResources
.RESOURCE_DATA
));
355 int resourceId
= resource
.getAsInteger(DavResources
._ID
);
356 if (collections
.containsKey(collectionId
)) {
357 AcalCollection collection
= collections
.get(collectionId
);
358 VComponent comp
= VComponent
.createComponentFromResource(resource
,collection
);
359 if (comp
== null || ! (comp
instanceof VCalendar
)) continue;
360 VCalendar node
= (VCalendar
)comp
;
361 calendars
.put(resourceId
,node
);
364 calendars
.remove(resourceId
);
366 } catch (VComponentCreationException e
) {
367 if (Constants
.LOG_DEBUG
) Log
.d(TAG
,"Calendar parsing failed: "+e
.getMessage());
369 } catch (Exception e
) {
370 Log
.e(TAG
, "Unknown error occured parsing calendar data: "+e
.getMessage());
371 Log
.e(TAG
, Log
.getStackTraceString(e
));
375 //Output useful information
376 if ( Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) {
378 Set
<Integer
> keys
= calendars
.keySet();
379 for (int key
: keys
) {
380 size
+= calendars
.get(key
).size();
382 Log
.println(Constants
.LOGD
, TAG
,
383 "Processed "+count
+" resources in : "+(System
.currentTimeMillis()-begin
)+"ms");
384 if ( Constants
.LOG_VERBOSE
) Log
.println(Constants
.LOGV
,TAG
,
385 "Now have "+size
+" Nodes created from "+calendars
.size()+" Calendar objects");
387 } catch (Exception e
) {
388 Log
.e(TAG
,"Uncaught exeception occured during resource processing: "+e
.getMessage());
389 Log
.getStackTraceString(e
);
396 * If our worker needs to be set - use this method. Do not interrupt or kill externally!
398 private void resetWorker() {
399 if (Constants
.LOG_DEBUG
) Log
.d(TAG
, "Reset worker called.");
400 this.interrupted
= true;
401 if (Thread
.currentThread() != worker
&& worker
!= null ) {
404 Log
.i(TAG
,"Resetting worker thread.");
406 worker
= new Thread(this);
410 //If you write a widget for acal, feel free to add update code here, but keep it nicely separated
411 private void updateWidgets() {
412 //this method is called to determine weather any aCal widgets need to be told to update.
413 //Strictly speaking this wasn't the way the Android people wanted widgets to be used
414 //but too bad, we want our widgets to update exactly when they need to - no more/ no less
416 /** Start 'Show Upcoming' Widget Logic */
417 if (Constants
.LOG_DEBUG
) {
418 Log
.d(TAG
,"Checking if Show Upcoming Widget(s) needs update.");
421 //step 1, get the next N events
422 AcalDateTime now
= new AcalDateTime().applyLocalTimeZone();
423 AcalDateTime later
= AcalDateTime
.addDays(now
, ShowUpcomingWidgetProvider
.NUM_DAYS_TO_LOOK_AHEAD
);
424 AcalDateRange range
= new AcalDateRange(now
,later
);
426 List
<AcalEvent
> events
= dataRequest
.getEventsForDateRange(range
);
427 Collections
.sort(events
);
428 while (events
.size() > ShowUpcomingWidgetProvider
.NUMBER_OF_EVENTS_TO_SHOW
) events
.remove(events
.size()-1);
430 //step 2, notify provider of new data
431 ShowUpcomingWidgetProvider
.checkIfUpdateRequired(this, events
);
433 } catch (RemoteException e
) {
434 //A remote exception from calling the method of an inner class? I don't think so.
435 if (Constants
.LOG_DEBUG
)
436 Log
.d(TAG
, "RemoteException during widget update?!?!");
438 /** End 'Show Upcoming' Widget Logic */
441 /** Calculates the next time an alarm will go off AFTER the lastTriggeredAlarmtime
442 * If no alarm is found alarms are disabled. Otherwise, set the alarm trigger for this time
444 private void updateAlarms() {
445 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Rebuilding alarm queue. Last triggered Time: "+lastTriggeredAlarmTime
);
447 //Avoid concurrent modification of active queue
448 synchronized(alarmQueue
) {
449 PriorityQueue
<AcalAlarm
> newQueue
= new PriorityQueue
<AcalAlarm
>();
451 //move all alarms that have the same time as lastTriggeredAlarmTime to this queue
452 while (!alarmQueue
.isEmpty() && alarmQueue
.peek().getNextTimeToFire().equals(lastTriggeredAlarmTime
)) newQueue
.offer(alarmQueue
.poll());
453 if (Constants
.LOG_DEBUG
)Log
.d(TAG
,"Transferred "+newQueue
.size()+" alarms from original queue that had timetofire=lasttrggeredtime");
455 //add All alarms for triggeredTime+1 to now+7D
456 AcalDateTime rangeStart
= lastTriggeredAlarmTime
.clone();
457 rangeStart
.applyLocalTimeZone(); // Ensure we have the correct timezone applied.
458 rangeStart
.addSeconds(1);
459 AcalDateTime currentTime
= new AcalDateTime();
460 currentTime
.applyLocalTimeZone(); // Ensure we have the correct timezone applied.
461 if ( rangeStart
.after(currentTime
) ) rangeStart
= currentTime
;
462 currentTime
.addDays(7);
463 AcalDateRange alarmRange
= new AcalDateRange( rangeStart
, currentTime
); //Only look forward 1 day
465 ArrayList
<AcalAlarm
> alarms
= this.getAlarmsForDateRange(alarmRange
);
466 if (Constants
.LOG_DEBUG
) Log
.d(TAG
, "Found "+alarms
.size()+" new alarms for range"+alarmRange
);
467 for (AcalAlarm alarm
: alarms
) { newQueue
.offer(alarm
); }
468 alarmQueue
= newQueue
;
470 } //end synchronisation block
471 if (Constants
.LOG_DEBUG
)Log
.d(TAG
,"Setting next alarm trigger");
472 setNextAlarmTrigger();
475 private void setNextAlarmTrigger() {
476 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "Set next alarm trigger called");
477 synchronized( alarmQueue
) {
478 AcalAlarm nextReg
= alarmQueue
.peek();
479 AcalAlarm nextSnooze
= snoozeQueue
.peek();
480 if ( nextReg
== null && nextSnooze
== null ) {
481 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "No alarms in alarm queues. Not setting any alarm trigger.");
482 // Stop existing alarm trigger
483 if ( alarmIntent
!= null ) alarmManager
.cancel(alarmIntent
);
487 if ( nextReg
== null ) {
488 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "No regular alarms, just snoozes.");
489 createAlarmIntent(nextSnooze
);
492 else if ( nextSnooze
== null ) {
493 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "No snooze alarms, just regulars.");
494 createAlarmIntent(nextReg
);
498 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "Both regular and Snooze alarms queued.");
499 if ( nextReg
.getNextTimeToFire().before(nextSnooze
.getNextTimeToFire()) ) createAlarmIntent(nextReg
);
501 createAlarmIntent(nextSnooze
);
507 // Changes the next alarm trigger time IFF its changed.
508 private synchronized void createAlarmIntent(AcalAlarm alarm
) {
509 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "Creating Alarm Intent for alarm: " + alarm
);
510 AcalDateTime now
= new AcalDateTime();
511 now
.applyLocalTimeZone();
512 long timeOfNextAlarm
= (now
.getDurationTo(alarm
.getNextTimeToFire())).getTimeMillis()
514 if ( this.alarmIntent
!= null ) {
515 if ( timeOfNextAlarm
== nextTriggerTime
) {
516 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "Alarm trigger time hasn't changed. Aborting.");
520 alarmManager
.cancel(alarmIntent
);
523 nextTriggerTime
= timeOfNextAlarm
;
524 Intent intent
= new Intent(this, AlarmActivity
.class);
525 alarmIntent
= PendingIntent
.getActivity(this, 0, intent
, PendingIntent
.FLAG_ONE_SHOT
);
526 alarmManager
.set(AlarmManager
.RTC_WAKEUP
, timeOfNextAlarm
, alarmIntent
);
527 //if ( Constants.LOG_DEBUG )
529 "Set alarm trigger for: " + timeOfNextAlarm
+ "/" + alarm
.getNextTimeToFire());
533 * Creates a list of all alarms that will occur in the specified date range
537 public ArrayList
<AcalAlarm
> getAlarmsForDateRange(AcalDateRange dateRange
) {
540 ArrayList
<AcalAlarm
> alarms
= new ArrayList
<AcalAlarm
>();
542 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
,"Looking for alarms in range "+dateRange
);
544 //temp map for checking alarm active status
545 Map
<Integer
,Boolean
> collectionAlarmsEnabled
= new HashMap
<Integer
,Boolean
>();
546 for ( VCalendar vc
: calendars
.values()) {
547 if ( collectionAlarmsEnabled
.containsKey(vc
.getCollectionId()) ) {
548 if ( !collectionAlarmsEnabled
.get(vc
.getCollectionId()) ) {
554 int id
= vc
.getCollectionId();
555 boolean alarmsEnabled
= (this.collections
.get(id
).getCollectionRow()
556 .getAsInteger(DavCollections
.USE_ALARMS
) == 1);
557 collectionAlarmsEnabled
.put(id
, alarmsEnabled
);
558 if ( !alarmsEnabled
) {
563 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Processing alarm");
565 vc
.setPersistentOn();
566 if ( vc
.hasAlarm() ) vc
.appendAlarmInstancesBetween(alarms
, dateRange
);
569 catch ( YouMustSurroundThisMethodInTryCatchOrIllEatYouException e
) {
573 vc
.setPersistentOff();
575 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Processing alarm");
578 Log
.d(TAG
,"Checked "+processed
+" resources for alarms, skipped "+skipped
);
579 processed
= skipped
= 0;
581 for ( VCalendar vc
: newResources
.values()) {
582 if ( collectionAlarmsEnabled
.containsKey(vc
.getCollectionId()) ) {
583 if ( !collectionAlarmsEnabled
.get(vc
.getCollectionId()) ) {
589 int id
= vc
.getCollectionId();
590 boolean alarmsEnabled
= (this.collections
.get(id
).getCollectionRow()
591 .getAsInteger(DavCollections
.USE_ALARMS
) == 1);
592 collectionAlarmsEnabled
.put(id
, alarmsEnabled
);
593 if ( !alarmsEnabled
) {
600 vc
.setPersistentOn();
601 List
<AcalEvent
> events
= new ArrayList
<AcalEvent
>();
602 if ( vc
.hasAlarm() && vc
.appendEventInstancesBetween(events
, dateRange
, true) ) {
603 for( AcalEvent event
: events
) {
604 for (AcalAlarm alarm
: event
.getAlarms()) {
605 // Since this is pending we need to ensure the alarm has the associated event data
606 alarm
.setEvent(event
);
613 catch (YouMustSurroundThisMethodInTryCatchOrIllEatYouException e
) {
616 vc
.setPersistentOff();
620 if ( Constants
.debugAlarms
) {
621 Log
.d(TAG
,"Checked "+processed
+" pending resources for alarms, skipped "+skipped
);
622 Log
.d(TAG
,"Got "+alarms
.size()+" alarms for "+dateRange
);
623 for( AcalAlarm al
: alarms
)
624 Log
.d(TAG
,al
.toString());
632 * Called to add a resource to queue - May be made public at a later date.
634 * @param resourceId The resource id that changed
635 * @param blob The new blob data.
637 private void resourceChanged(ContentValues cv
) {
638 if (Constants
.debugCalendarDataService
&& Constants
.LOG_VERBOSE
) Log
.println(Constants
.LOGV
, TAG
,
639 "Received notification of changed resource: "+cv
.getAsInteger(DavResources
._ID
));
640 String blob
= cv
.getAsString(DavResources
.RESOURCE_DATA
);
641 if (blob
== null || blob
.equalsIgnoreCase("")) {
642 if (Constants
.debugCalendarDataService
&& Constants
.LOG_VERBOSE
) Log
.println(Constants
.LOGV
, TAG
,
643 "Changed resource has no blob data. Ignoring.");
646 Long latestEnd
= cv
.getAsLong(DavResources
.LATEST_END
);
647 if ( latestEnd
!= null && earlyTimeStamp
.after(AcalDateTime
.fromMillis(latestEnd
)) ) {
648 if (Constants
.debugCalendarDataService
&& Constants
.LOG_VERBOSE
) Log
.println(Constants
.LOGV
, TAG
,
649 "Changed resource is before earlyTimeStamp. Ignoring.");
652 resourcesPending
.offer(cv
);
656 private void openUnlessInTx() {
657 if ( inResourceTx
> 0 ) {
658 if ( System
.currentTimeMillis() < inResourceTx
) {
666 private void pendingResourceDeleted(int rowid
) {
667 if (this.newResources
.containsKey(rowid
)) newResources
.remove(rowid
);
668 if ( Constants
.LOG_DEBUG
) Log
.println(Constants
.LOGD
,TAG
,
669 "Pending resource removed.");
675 * When a collection is deleted, throw away all of the events in it. We can't just
676 * find the resourceId from the database, because we might be out of sync, and would
677 * leave crud lying around. Possibly some kind of index might be good here, but on
678 * the other hand this is a low-frequency operation, across a relatively small in-memory
681 * @param collectionId
683 private void collectionDeleted( int collectionId
) {
685 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) {
686 AcalCollection c
= collections
.get(collectionId
);
687 String displayName
= (c
==null?
"unknown collection" : c
.getCollectionRow().getAsString(DavCollections
.DISPLAYNAME
));
688 Log
.d(TAG
, "Collection "+collectionId
+" '"+displayName
+"' was removed/disabled.");
691 for( Entry
<Integer
, VCalendar
> vCal
: calendars
.entrySet() ) {
692 if ( vCal
.getValue().getCollectionId() == collectionId
) {
693 calendars
.remove(vCal
.getKey());
696 collections
.remove(collectionId
);
702 * When a collection is created we need to create it in our table, and then load it up
705 * @param collectionData
707 private void collectionCreated( ContentValues collectionData
) {
708 int collectionId
= collectionData
.getAsInteger(DavCollections
._ID
);
710 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Collection "+collectionId
711 +" '"+collectionData
.getAsString(DavCollections
.DISPLAYNAME
)
712 +"' was added/enabled.");
714 AcalCollection c
= new AcalCollection(collectionData
);
715 collections
.put(collectionId
, c
);
716 if ( c
.useForEvents
)
717 addEventsForCollection(collectionId
);
719 addTodosForCollection(collectionId
);
720 if ( c
.useForJournal
)
721 addJournalsForCollection(collectionId
);
727 * When a collection is updated, we need to update our internal information about that
728 * collection. Note that we update the data inside the existing structure, so that all
729 * of the VCalendars referencing into that structure for their colour will be magically
732 * @param collectionData
734 private void collectionUpdated( ContentValues collectionData
) {
735 int collectionId
= collectionData
.getAsInteger(DavCollections
._ID
);
737 if (Constants
.LOG_DEBUG
)
738 Log
.d(TAG
, "Collection "+collectionId
739 +" '"+collectionData
.getAsString(DavCollections
.DISPLAYNAME
)
742 AcalCollection c
= collections
.get(collectionId
);
745 collectionCreated(collectionData
);
747 c
.updateCollectionRow(collectionData
);
755 * When we first start, we need to set up all collections from the database.
758 private synchronized void setupCollections() {
759 Cursor cursor
= this.getContentResolver().query( DavCollections
.CONTENT_URI
, null,
760 "("+DavCollections
.ACTIVE_EVENTS
761 +" OR "+DavCollections
.ACTIVE_TASKS
762 +" OR "+DavCollections
.ACTIVE_JOURNAL
763 +") AND EXISTS(SELECT 1 FROM "+Servers
.DATABASE_TABLE
+" WHERE "+Servers
.ACTIVE
764 +" AND "+Servers
._ID
+"="+DavCollections
.DATABASE_TABLE
+"."+DavCollections
.SERVER_ID
+")"
766 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
)
767 Log
.i(TAG
, "Initialising "+cursor
.getCount()+" collections.");
769 for ( cursor
.moveToFirst(); !cursor
.isAfterLast(); cursor
.moveToNext() ) {
770 ContentValues cv
= new ContentValues();
771 DatabaseUtils
.cursorRowToContentValues(cursor
, cv
);
772 collectionCreated(cv
);
775 catch ( Exception e
) {
776 Log
.w(TAG
,Log
.getStackTraceString(e
));
779 if ( ! cursor
.isClosed() ) cursor
.close();
784 private void addEventsForCollection( int collectionId
) {
785 //get all current VEVENT resources and add them to the queue
786 Cursor cursor
= this.getContentResolver().query(DavResources
.CONTENT_URI
, null,
787 DavResources
.COLLECTION_ID
+"="+collectionId
788 +" AND "+DavResources
.EFFECTIVE_TYPE
+"="+DavResources
.TYPE_EVENT
789 +" AND (latest_end > "+Long
.toString(earlyTimeStamp
.getMillis())+" OR latest_end IS NULL)",
791 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) Log
.d(TAG
, "Adding "+cursor
.getCount()+" resources to pending queue.");
792 for( cursor
.moveToFirst(); !cursor
.isAfterLast(); cursor
.moveToNext() ) {
793 ContentValues cv
= new ContentValues();
794 DatabaseUtils
.cursorRowToContentValues(cursor
, cv
);
795 String blob
= cursor
.getString(cursor
.getColumnIndex(DavResources
.RESOURCE_DATA
));
796 if (blob
!= null && !blob
.equalsIgnoreCase("")) {
797 resourcesPending
.offer(cv
);
804 private void addTodosForCollection( int collectionId
) {
805 //get all current resources and add them to the queue
806 Cursor cursor
= this.getContentResolver().query(DavResources
.CONTENT_URI
, null,
807 DavResources
.COLLECTION_ID
+"="+collectionId
808 +" AND "+DavResources
.EFFECTIVE_TYPE
+"="+DavResources
.TYPE_TASK
809 +" AND (latest_end > "+Long
.toString(earlyTimeStamp
.getMillis())+" OR latest_end IS NULL)",
811 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) Log
.d(TAG
, "Adding "+cursor
.getCount()+" resources to pending queue.");
812 for( cursor
.moveToFirst(); !cursor
.isAfterLast(); cursor
.moveToNext() ) {
813 ContentValues cv
= new ContentValues();
814 DatabaseUtils
.cursorRowToContentValues(cursor
, cv
);
815 String blob
= cursor
.getString(cursor
.getColumnIndex(DavResources
.RESOURCE_DATA
));
816 if (blob
!= null && !blob
.equalsIgnoreCase("")) {
817 resourcesPending
.offer(cv
);
824 private void addJournalsForCollection( int collectionId
) {
825 //get all current resources and add them to the queue
826 Cursor cursor
= this.getContentResolver().query(DavResources
.CONTENT_URI
, null,
827 DavResources
.COLLECTION_ID
+"="+collectionId
828 +" AND "+DavResources
.EFFECTIVE_TYPE
+"="+DavResources
.TYPE_JOURNAL
829 +" AND (latest_end > "+Long
.toString(earlyTimeStamp
.getMillis())+" OR latest_end IS NULL)",
831 if (Constants
.debugCalendarDataService
&& Constants
.LOG_DEBUG
) Log
.d(TAG
, "Adding "+cursor
.getCount()+" resources to pending queue.");
832 for( cursor
.moveToFirst(); !cursor
.isAfterLast(); cursor
.moveToNext() ) {
833 ContentValues cv
= new ContentValues();
834 DatabaseUtils
.cursorRowToContentValues(cursor
, cv
);
835 String blob
= cursor
.getString(cursor
.getColumnIndex(DavResources
.RESOURCE_DATA
));
836 if (blob
!= null && !blob
.equalsIgnoreCase("")) {
837 resourcesPending
.offer(cv
);
844 private void fetchOldResources( AcalDateTime previousTimeStamp
) {
845 StringBuilder whereClause
= new StringBuilder(DavResources
.COLLECTION_ID
);
846 whereClause
.append(" IN (");
847 boolean pastFirst
= false;
848 for( int collectionId
: collections
.keySet() ) {
849 whereClause
.append( (pastFirst?
", ":"") );
850 whereClause
.append(Integer
.toString(collectionId
));
853 whereClause
.append(") AND latest_end > ");
854 whereClause
.append(Long
.toString(earlyTimeStamp
.getMillis()));
855 whereClause
.append(" AND latest_end < ");
856 whereClause
.append(Long
.toString(previousTimeStamp
.getMillis()));
857 Cursor cursor
= this.getContentResolver().query(DavResources
.CONTENT_URI
, null, whereClause
.toString(), null, null);
858 // if (Constants.debugCalendarDataService && Constants.LOG_DEBUG)
859 Log
.d(TAG
, "Fetching old resources now possibly useful in view: "
860 + cursor
.getCount() + " records to process.");
861 for( cursor
.moveToFirst(); !cursor
.isAfterLast(); cursor
.moveToNext() ) {
862 ContentValues cv
= new ContentValues();
863 DatabaseUtils
.cursorRowToContentValues(cursor
, cv
);
864 String blob
= cursor
.getString(cursor
.getColumnIndex(DavResources
.RESOURCE_DATA
));
865 if (blob
!= null && !blob
.equalsIgnoreCase("")) {
866 resourcesPending
.offer(cv
);
873 private void discardOldResources() {
874 // if (Constants.debugCalendarDataService && Constants.LOG_DEBUG)
875 Log
.d(TAG
, "Discarding old resources not useful in view.");
877 for( Entry
<Integer
, VCalendar
> vCal
: calendars
.entrySet() ) {
878 if ( earlyTimeStamp
.after(vCal
.getValue().getRangeEnd()) ) {
879 calendars
.remove(vCal
.getKey());
884 /********************************************************
885 * Interface Overrides *
886 ********************************************************/
889 * React to database change notifications.
892 public void databaseChanged(DatabaseChangedEvent changeEvent
) {
893 ContentValues cv
= changeEvent
.getContentValues();
894 if (changeEvent
.getEventType() == DatabaseChangedEvent
.DATABASE_SHOW_UPCOMING_WIDGET_UPDATE
) updateWidgets();
895 else if (changeEvent
.getEventType() == DatabaseChangedEvent
.DATABASE_BEGIN_RESOURCE_CHANGES
) {
896 this.inResourceTx
= System
.currentTimeMillis() + MAX_TX_AGE
;
897 this.changesDuringTx
= false;
900 else if (changeEvent
.getEventType() == DatabaseChangedEvent
.DATABASE_END_RESOURCE_CHANGES
) {
901 this.inResourceTx
= 0;
903 else if (changeEvent
.getEventType() == DatabaseChangedEvent
.DATABASE_INVALIDATED
) {
904 if (Constants
.LOG_DEBUG
) Log
.println(Constants
.LOGD
,TAG
,
905 "Database invalidated message received. Clearing memory.");
906 resourcesPending
= new ConcurrentLinkedQueue
<ContentValues
>();
907 calendars
= new ConcurrentHashMap
<Integer
,VCalendar
>();
908 collections
= new ConcurrentHashMap
<Integer
,AcalCollection
>();
909 newResources
= new ConcurrentHashMap
<Integer
,VCalendar
>();
913 this.inResourceTx
= 0;
915 else if (changeEvent
.getTable() == DavResources
.class) {
916 if ( Constants
.debugCalendarDataService
&& Constants
.LOG_VERBOSE
)
917 Log
.v(TAG
, "Received notification of Resources Table change.");
918 switch (changeEvent
.getEventType()) {
919 case DatabaseChangedEvent
.DATABASE_RECORD_DELETED
:
920 calendars
.remove(cv
.getAsInteger(DavResources
._ID
));
922 case DatabaseChangedEvent
.DATABASE_RECORD_UPDATED
:
923 this.resourceChanged(cv
);
925 case DatabaseChangedEvent
.DATABASE_RECORD_INSERTED
:
926 this.resourceChanged(cv
);
930 else if ( changeEvent
.getTable() == DavCollections
.class ) {
931 if ( Constants
.debugCalendarDataService
&& Constants
.LOG_VERBOSE
)
932 Log
.v(TAG
, "Received notification of Collections Table change.");
935 boolean active
= false;
936 boolean exists
= false;
938 if (cv
.containsKey(DavCollections
._ID
)) {
939 if (cv
.getAsInteger(DavCollections
._ID
) != null)
940 id
= cv
.getAsInteger(DavCollections
._ID
);
942 if (cv
.containsKey(DavCollections
.ACTIVE_EVENTS
)) {
943 if (cv
.getAsInteger(DavCollections
.ACTIVE_EVENTS
) != null)
944 active
= cv
.getAsInteger(DavCollections
.ACTIVE_EVENTS
) == 1;
946 exists
= (id
!= -1) && this.collections
.containsKey(id
);
948 switch (changeEvent
.getEventType()) {
949 case DatabaseChangedEvent
.DATABASE_RECORD_DELETED
:
950 if (exists
) this.collectionDeleted(cv
.getAsInteger(DavResources
._ID
));
952 case DatabaseChangedEvent
.DATABASE_RECORD_UPDATED
:
953 if (!exists
&& active
) collectionCreated(cv
);
954 else if (exists
&& !active
) collectionDeleted(id
);
955 else if (exists
) this.collectionUpdated(cv
);
957 case DatabaseChangedEvent
.DATABASE_RECORD_INSERTED
:
958 if (!exists
) this.collectionCreated(cv
);
959 else this.collectionUpdated(cv
);
962 throw new IllegalArgumentException();
966 else if (changeEvent
.getTable() == PendingChanges
.class) {
967 if (Constants
.debugCalendarDataService
&& Constants
.LOG_VERBOSE
)
968 Log
.v(TAG
, "Received notification of pendingChanges Table Change.");
969 switch (changeEvent
.getEventType()) {
970 case DatabaseChangedEvent
.DATABASE_RECORD_DELETED
:
971 this.pendingResourceDeleted(cv
.getAsInteger(PendingChanges
._ID
));
976 if ( this.inResourceTx
== 0 ) {
977 // Always flush the cache if stuff has changed.
978 if ( this.changesDuringTx
) {
980 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Requesting flushing of event cache.");
981 dataRequest
.flushCache();
982 if (callback
!= null) callback
.statusChanged(UPDATE
, false);
984 catch (RemoteException e
) {
985 Log
.d(TAG
,Log
.getStackTraceString(e
));
988 this.openUnlessInTx();
991 this.changesDuringTx
= true;
997 * Main worker thread execution loop
1001 Log
.i(TAG
, "Main worker thread started.");
1005 if (Constants
.LOG_DEBUG
) Log
.d(TAG
, "Records added to queue. Starting main loop.");
1007 while (this.worker
== Thread
.currentThread()) {
1008 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Processing pending resources");
1009 if ( this.processPendingResources() ) {
1010 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Updating alarms");
1014 if (callback
!= null) {
1016 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Flushing cache");
1017 dataRequest
.flushCache();
1018 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Notifying status change via callback");
1019 callback
.statusChanged(UPDATE
, false);
1020 } catch (RemoteException e
) {
1024 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Finished worker run");
1026 this.threadHolder
.close();
1028 Runtime r
= Runtime
.getRuntime();
1029 if ( (r
.totalMemory() * 100) / r
.maxMemory() > 125 ) {
1030 Log
.w(TAG
, "Closing down CalendarDataService - out of memory!");
1031 this.interrupted
= true;
1032 if (Thread
.currentThread() != worker
) worker
.interrupt();
1037 this.threadHolder
.block();
1040 catch (Exception e
) {
1041 if (this.interrupted
) return;
1042 if (Constants
.LOG_DEBUG
) {
1043 if (Constants
.LOG_DEBUG
) {
1044 Log
.println(Constants
.LOGD
, TAG
,
1045 "Data Thread stopped unexpectedly: "+e
.getMessage()+"\n\t\tAttempting to restart.");
1046 Log
.println(Constants
.LOGD
, TAG
, Log
.getStackTraceString(e
));
1049 this.openUnlessInTx();
1055 /****************************************************
1057 ****************************************************/
1060 * Activity communication interface
1062 * @author Morphoss Ltd
1065 private class DataRequestHandler
extends DataRequest
.Stub
{
1067 private EventCache eventCache
= new EventCache();
1069 //EventCache methods
1070 public synchronized List
<SimpleAcalEvent
> getEventsForDay(AcalDateTime day
) {
1071 eventCache
.addDay(day
,this);
1072 return eventCache
.getEventsForDay(day
,this);
1074 public synchronized List
<SimpleAcalEvent
> getEventsForDays(AcalDateRange range
) {
1075 return eventCache
.getEventsForDays(range
,this);
1077 public synchronized int getNumberEventsForDay(AcalDateTime day
) {
1078 eventCache
.addDay(day
,this);
1079 return eventCache
.getNumberEventsForDay(day
);
1081 public synchronized SimpleAcalEvent
getNthEventForDay(AcalDateTime day
, int n
) {
1082 eventCache
.addDay(day
,this);
1083 return eventCache
.getNthEventForDay(day
, n
);
1085 public synchronized void deleteEvent(AcalDateTime day
, int n
) {
1086 eventCache
.deleteEvent(day
, n
);
1087 eventCache
.addDay(day
,this);
1089 public synchronized void flushCache() {
1090 eventCache
.flushCache();
1092 public synchronized void flushDay( AcalDateTime day
) {
1093 eventCache
.flushDay(day
,this);
1098 public void resetCache() {
1099 resourcesPending
= new ConcurrentLinkedQueue
<ContentValues
>();
1100 calendars
= new ConcurrentHashMap
<Integer
,VCalendar
>();
1101 collections
= new ConcurrentHashMap
<Integer
,AcalCollection
>();
1102 newResources
= new ConcurrentHashMap
<Integer
,VCalendar
>();
1108 public List
<AcalEvent
> getEventsForDateRange(AcalDateRange dateRange
) throws RemoteException
{
1109 List
<AcalEvent
> events
= new ArrayList
<AcalEvent
>();
1110 for (VCalendar vc
: calendars
.values()) {
1111 vc
.appendEventInstancesBetween(events
, dateRange
, false);
1114 // display newly added events
1115 for (VCalendar vc
: newResources
.values()) {
1116 vc
.appendEventInstancesBetween(events
, dateRange
, true);
1118 updateEarlyTimeStamp(dateRange
.start
);
1124 public boolean isInitialising() {
1129 public boolean isProcessing() {
1130 return processingNewData
;
1133 public void registerCallback(DataRequestCallBack cb
) {
1137 public void unregisterCallback(DataRequestCallBack cb
) {
1142 * Returns all the alarms that should have occurred between lastriggeredtime + 1 sec and now
1144 * @throws RemoteException
1147 public AcalAlarm
getCurrentAlarm() throws RemoteException
{
1148 synchronized (alarmQueue
) {
1149 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Alarm activity has requested the next alarm");
1150 AcalAlarm nextReg
= alarmQueue
.peek();
1151 AcalAlarm nextSnooze
= snoozeQueue
.peek();
1152 AcalAlarm ret
= null;
1153 if (nextReg
== null && nextSnooze
== null) {
1154 if (Constants
.LOG_DEBUG
)Log
.d(TAG
,"No alarms in queue. Returning null.");
1156 } else if (nextSnooze
== null) {
1157 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Only regular alarm in queue. Returning alarm: "+nextReg
);
1159 } else if (nextReg
== null) {
1160 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Only snooze alarm in queue. Returning alarm: "+nextSnooze
);
1163 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Both snooze and regular alarms available. Calculating which one has prioty\n"+
1164 "\t\tReg alarm time: "+nextReg
.getNextTimeToFire()+"\n"+
1165 "\t\tSnooze alarm time: "+nextSnooze
.getNextTimeToFire());
1166 if (nextReg
.compareTo(nextSnooze
) <= 0) {
1167 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Next regular alarm has priority. Returning alarm: "+nextReg
);
1170 if (Constants
.LOG_DEBUG
)Log
.d(TAG
, "Next Snooze alarm has priority. Returning alarm: "+nextSnooze
);
1176 setNextAlarmTrigger(); //calculate next trigger time
1179 AcalDateTime now
= new AcalDateTime();
1180 now
.applyLocalTimeZone();
1181 if (ret
.getNextTimeToFire().after(now
)) {
1182 if (Constants
.LOG_DEBUG
) Log
.d(TAG
, "Next alarm is not due to fire until "+ret
.getNextTimeToFire()+" returning null and ensuring next trigger is set.");
1183 setNextAlarmTrigger();
1189 //Removes an alarm from its queue - called IFF the user has responded to the alarm
1190 //Updates the last triggered time
1191 private void triggeredAlarmDismissedByUser(AcalAlarm alarm
) {
1192 synchronized(alarmQueue
) {
1193 lastTriggeredAlarmTime
= alarm
.getNextTimeToFire();
1194 if (alarm
.isSnooze
) snoozeQueue
.remove(alarm
);
1195 else alarmQueue
.remove(alarm
);
1203 public void eventChanged(AcalEvent action
) throws RemoteException
{
1204 int collectionId
= action
.getCollectionId();
1205 AcalCollection collection
= collections
.get(collectionId
);
1206 Log
.i(TAG
,"Event changed: "+action
.getSummary()+", "+action
.getStart().toPropertyString(PropertyName
.DTSTART
));
1208 switch (action
.getAction()) {
1209 case AcalEvent
.ACTION_CREATE
: {
1210 VCalendar newCal
= VCalendar
.getGenericCalendar(collection
, action
);
1211 action
.setAction(AcalEvent
.ACTION_MODIFY_ALL
);
1212 newCal
.applyEventAction(action
);
1213 ContentValues cv
= new ContentValues();
1214 cv
.put(PendingChanges
.COLLECTION_ID
, collectionId
);
1215 cv
.put(PendingChanges
.OLD_DATA
, "");
1216 cv
.put(PendingChanges
.NEW_DATA
, newCal
.getOriginalBlob());
1217 Uri row
= getContentResolver().insert(PendingChanges
.CONTENT_URI
, cv
);
1218 int r
= Integer
.parseInt(row
.getLastPathSegment());
1219 // add to pending map
1220 newResources
.put(r
, newCal
);
1223 case AcalEvent
.ACTION_MODIFY_ALL
:
1224 case AcalEvent
.ACTION_MODIFY_SINGLE
:
1225 case AcalEvent
.ACTION_MODIFY_ALL_FUTURE
:
1226 case AcalEvent
.ACTION_DELETE_SINGLE
:
1227 case AcalEvent
.ACTION_DELETE_ALL_FUTURE
: {
1228 int rid
= action
.getResourceId();
1229 VCalendar original
= calendars
.get(rid
);
1230 String newBlob
= original
.applyEventAction(action
);
1231 VCalendar changedResource
= (VCalendar
) VComponent
.createComponentFromBlob(newBlob
, rid
, collection
);
1232 if (newBlob
== null || newBlob
.equalsIgnoreCase(""))
1233 throw new IllegalStateException(
1234 "Blob creation resulted in null or empty string during modify event");
1235 ContentValues cv
= new ContentValues();
1236 cv
.put(PendingChanges
.COLLECTION_ID
, collectionId
);
1237 cv
.put(PendingChanges
.RESOURCE_ID
, rid
);
1238 cv
.put(PendingChanges
.OLD_DATA
, action
.getOriginalBlob());
1239 cv
.put(PendingChanges
.NEW_DATA
, newBlob
);
1240 getContentResolver().insert(PendingChanges
.CONTENT_URI
, cv
);
1241 calendars
.put(rid
,changedResource
);
1244 case AcalEvent
.ACTION_DELETE_ALL
: {
1245 int rid
= action
.getResourceId();
1246 ContentValues cv
= new ContentValues();
1247 cv
.put(PendingChanges
.COLLECTION_ID
, collectionId
);
1248 cv
.put(PendingChanges
.RESOURCE_ID
, rid
);
1249 cv
.put(PendingChanges
.OLD_DATA
, action
.getOriginalBlob());
1250 cv
.putNull(PendingChanges
.NEW_DATA
);
1251 getContentResolver().insert(PendingChanges
.CONTENT_URI
, cv
);
1252 calendars
.remove(rid
);
1256 throw new IllegalArgumentException("Invalid event action");
1260 ServiceJob sj
= new SyncChangesToServer();
1261 sj
.TIME_TO_EXECUTE
= 100;
1262 WorkerClass
.getExistingInstance().addJobAndWake(sj
);
1264 catch (Exception e
) {
1265 Log
.e(TAG
, "Error starting sync job for event modification.");
1270 public void dismissAlarm(AcalAlarm alarm
) throws RemoteException
{
1271 this.triggeredAlarmDismissedByUser(alarm
);
1276 public void snoozeAlarm(AcalAlarm alarm
) throws RemoteException
{
1277 this.triggeredAlarmDismissedByUser(alarm
);
1278 SharedPreferences prefs
= PreferenceManager
.getDefaultSharedPreferences(CalendarDataService
.this);
1279 String snoozeTime
= prefs
.getString(CalendarDataService
.this.getString(R
.string
.prefSnoozeDuration
), "5");
1280 alarm
.snooze(new AcalDuration("PT"+snoozeTime
+"M"));
1281 alarm
.setToLocalTime();
1282 Toast
.makeText(CalendarDataService
.this, "Alarm Snoozed for "+snoozeTime
+" Minutes", Toast
.LENGTH_LONG
);
1283 snoozeQueue
.offer(alarm
);
1284 setNextAlarmTrigger();
1289 private TodoList todoItems
= new TodoList();
1292 public List
<SimpleAcalTodo
> getTodos(boolean listCompleted
, boolean listFuture
) throws RemoteException
{
1293 todoItems
.reset(listCompleted
, listFuture
);
1294 for (VCalendar vc
: calendars
.values() ) {
1295 Masterable master
= vc
.getMasterChild();
1296 if ( master
instanceof VTodo
) {
1297 todoItems
.add( new SimpleAcalTodo(master
, false) );
1301 return todoItems
.getList();
1305 public SimpleAcalTodo
getNthTodo(int n
) throws RemoteException
{
1306 return todoItems
.getNth(n
);
1310 public int getNumberTodos() throws RemoteException
{
1311 return todoItems
.count();
1315 public void todoChanged(VCalendar changedResource
, int action
) throws RemoteException
{
1316 int collectionId
= changedResource
.getCollectionId();
1318 if ( Constants
.LOG_DEBUG
)
1319 Log
.d(TAG
, "Changed VTODO in collection "+collectionId
+" - "+changedResource
.getCollectionName());
1322 case TodoEdit
.ACTION_CREATE
: {
1323 ContentValues cv
= new ContentValues();
1324 cv
.put(PendingChanges
.COLLECTION_ID
, collectionId
);
1325 cv
.put(PendingChanges
.OLD_DATA
, "");
1326 cv
.put(PendingChanges
.NEW_DATA
, changedResource
.getCurrentBlob());
1327 Uri row
= getContentResolver().insert(PendingChanges
.CONTENT_URI
, cv
);
1328 int r
= Integer
.parseInt(row
.getLastPathSegment());
1329 // add to pending map
1330 newResources
.put(r
, changedResource
);
1334 case TodoEdit
.ACTION_COMPLETE
:
1335 Masterable vTodo
= changedResource
.getMasterChild();
1336 if ( vTodo
instanceof VTodo
) {
1337 ((VTodo
) vTodo
).setCompleted(new AcalDateTime());
1338 ((VTodo
) vTodo
).setPercentComplete(100);
1339 ((VTodo
) vTodo
).setStatus(VTodo
.Status
.COMPLETED
);
1341 case TodoEdit
.ACTION_MODIFY_ALL
:
1342 case TodoEdit
.ACTION_MODIFY_SINGLE
:
1343 case TodoEdit
.ACTION_MODIFY_ALL_FUTURE
:
1344 case TodoEdit
.ACTION_DELETE_SINGLE
:
1345 case TodoEdit
.ACTION_DELETE_ALL_FUTURE
: {
1346 int rid
= changedResource
.getResourceId();
1347 VCalendar original
= calendars
.get(rid
);
1348 String newBlob
= changedResource
.getCurrentBlob();
1349 if (newBlob
== null || newBlob
.equalsIgnoreCase(""))
1350 throw new IllegalStateException(
1351 "Blob creation resulted in null or empty string during modify event");
1352 ContentValues cv
= new ContentValues();
1353 cv
.put(PendingChanges
.COLLECTION_ID
, collectionId
);
1354 cv
.put(PendingChanges
.RESOURCE_ID
, rid
);
1355 cv
.put(PendingChanges
.OLD_DATA
, original
.getOriginalBlob());
1356 cv
.put(PendingChanges
.NEW_DATA
, newBlob
);
1357 getContentResolver().insert(PendingChanges
.CONTENT_URI
, cv
);
1358 calendars
.put(rid
, changedResource
);
1361 case TodoEdit
.ACTION_DELETE_ALL
: {
1362 int rid
= changedResource
.getResourceId();
1363 ContentValues cv
= new ContentValues();
1364 cv
.put(PendingChanges
.COLLECTION_ID
, collectionId
);
1365 cv
.put(PendingChanges
.RESOURCE_ID
, rid
);
1366 cv
.put(PendingChanges
.OLD_DATA
, changedResource
.getOriginalBlob());
1367 cv
.putNull(PendingChanges
.NEW_DATA
);
1368 getContentResolver().insert(PendingChanges
.CONTENT_URI
, cv
);
1369 calendars
.remove(rid
);
1373 throw new IllegalArgumentException("Invalid change action");
1377 ServiceJob sj
= new SyncChangesToServer();
1378 sj
.TIME_TO_EXECUTE
= 100;
1379 WorkerClass
.getExistingInstance().addJobAndWake(sj
);
1381 catch (Exception e
) {
1382 Log
.e(TAG
, "Error starting sync job for event modification.");