Fix final issues with timezone offsets in widget.
[acal.git] / src / com / morphoss / acal / dataservice / CalendarDataService.java
blobd1704adedb6c43b57bf6e384caa233b395e1ca6a
1 /*
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;
21 import java.io.File;
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;
32 import java.util.Map;
33 import java.util.PriorityQueue;
34 import java.util.Queue;
35 import java.util.Set;
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;
89 /**
90 * <p>
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.
94 * </p>
95 * <p>
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.
98 * </p>
99 * <p>
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.
104 * </p>
105 * <p>
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.
108 * </p>
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 *****************************************/
160 @Override
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);
172 //load state info
173 this.loadState();
175 //Get our worker thread ready for action
176 this.threadHolder.close();
177 if (worker == null) {
178 worker = new Thread(this);
179 worker.start();
183 //@Override
184 public IBinder onBind(Intent arg0) {
185 return dataRequest;
188 public DataRequest.Stub getDataRequest() {
189 return this.dataRequest;
192 @Override
193 public void onDestroy() {
194 this.saveState();
195 super.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
208 Object sq = null;
209 try {
210 File f = new File(STATE_FILE);
211 if (!f.exists()) {
212 //File does not exist.
213 if (Constants.LOG_DEBUG)Log.d(TAG, "No state file to load.");
214 return;
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());
225 } finally {
226 //Close the ObjectOutputStream
227 try {
228 if (inputStream != null)
229 inputStream.close();
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() {
248 //Save state
249 if (Constants.LOG_DEBUG)Log.d(TAG, "Writing cds state to file. Last Triggered Time: "+lastTriggeredAlarmTime);
250 ObjectOutputStream outputStream = null;
251 try {
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));
264 } finally {
265 //Close the ObjectOutputStream
266 try {
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));
280 *<p>
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.
283 * </p>
284 * @param dateRange
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);
316 else
317 discardOldResources();
319 openUnlessInTx();
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()) {
334 return false;
337 int count = 0;
338 try {
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");
344 count++;
345 try {
346 //Remove all line wrapping prior to parsing
347 //create calendar object
348 try {
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);
363 else {
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 ) {
377 int size = 0;
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);
392 return count > 0;
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 ) {
402 worker.interrupt();
404 Log.i(TAG,"Resetting worker thread.");
405 this.worker = null;
406 worker = new Thread(this);
407 worker.start();
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.");
420 try {
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);
484 alarmIntent = null;
485 return;
487 if ( nextReg == null ) {
488 if ( Constants.LOG_DEBUG ) Log.d(TAG, "No regular alarms, just snoozes.");
489 createAlarmIntent(nextSnooze);
490 return;
492 else if ( nextSnooze == null ) {
493 if ( Constants.LOG_DEBUG ) Log.d(TAG, "No snooze alarms, just regulars.");
494 createAlarmIntent(nextReg);
495 return;
497 else {
498 if ( Constants.LOG_DEBUG ) Log.d(TAG, "Both regular and Snooze alarms queued.");
499 if ( nextReg.getNextTimeToFire().before(nextSnooze.getNextTimeToFire()) ) createAlarmIntent(nextReg);
500 else
501 createAlarmIntent(nextSnooze);
502 return;
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()
513 + now.getMillis();
514 if ( this.alarmIntent != null ) {
515 if ( timeOfNextAlarm == nextTriggerTime ) {
516 if ( Constants.LOG_DEBUG ) Log.d(TAG, "Alarm trigger time hasn't changed. Aborting.");
517 return; // no change
519 else {
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 )
528 Log.d(TAG,
529 "Set alarm trigger for: " + timeOfNextAlarm + "/" + alarm.getNextTimeToFire());
533 * Creates a list of all alarms that will occur in the specified date range
534 * @param dateRange
535 * @return
537 public ArrayList<AcalAlarm> getAlarmsForDateRange(AcalDateRange dateRange) {
538 int processed = 0;
539 int skipped = 0;
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()) ) {
549 skipped++;
550 continue;
553 else {
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 ) {
559 skipped++;
560 continue;
563 if ( Constants.debugHeap ) AcalDebug.heapDebug(TAG, "Processing alarm");
564 try {
565 vc.setPersistentOn();
566 if ( vc.hasAlarm() ) vc.appendAlarmInstancesBetween(alarms, dateRange);
567 processed++;
569 catch ( YouMustSurroundThisMethodInTryCatchOrIllEatYouException e ) {
572 finally {
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()) ) {
584 skipped++;
585 continue;
588 else {
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 ) {
594 skipped++;
595 continue;
599 try {
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);
607 alarms.add(alarm);
609 processed++;
613 catch (YouMustSurroundThisMethodInTryCatchOrIllEatYouException e) {
615 finally {
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());
628 return alarms;
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.");
644 return;
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.");
650 return;
652 resourcesPending.offer(cv);
653 openUnlessInTx();
656 private void openUnlessInTx() {
657 if ( inResourceTx > 0 ) {
658 if ( System.currentTimeMillis() < inResourceTx) {
659 inResourceTx = 0;
660 return;
663 threadHolder.open();
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.");
670 openUnlessInTx();
674 * <p>
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
679 * list.
680 * </p>
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);
701 * <p>
702 * When a collection is created we need to create it in our table, and then load it up
703 * from the database.
704 * </p>
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);
718 if ( c.useForTasks )
719 addTodosForCollection(collectionId);
720 if ( c.useForJournal )
721 addJournalsForCollection(collectionId);
726 * <p>
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
730 * changed.
731 * </p>
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)
740 +"' was modified.");
742 AcalCollection c = collections.get(collectionId);
744 if ( c == null )
745 collectionCreated(collectionData);
746 else
747 c.updateCollectionRow(collectionData);
754 * <p>
755 * When we first start, we need to set up all collections from the database.
756 * </p>
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+")"
765 , null, null);
766 if (Constants.debugCalendarDataService && Constants.LOG_DEBUG)
767 Log.i(TAG, "Initialising "+cursor.getCount()+" collections.");
768 try {
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));
778 finally {
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)",
790 null, 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);
800 cursor.close();
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)",
810 null, 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);
820 cursor.close();
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)",
830 null, 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);
840 cursor.close();
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));
851 pastFirst = true;
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);
869 cursor.close();
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.
891 @Override
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;
898 return;
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>();
910 setupCollections();
911 resetWorker();
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));
921 break;
922 case DatabaseChangedEvent.DATABASE_RECORD_UPDATED :
923 this.resourceChanged(cv);
924 break;
925 case DatabaseChangedEvent.DATABASE_RECORD_INSERTED:
926 this.resourceChanged(cv);
927 break;
930 else if ( changeEvent.getTable() == DavCollections.class ) {
931 if ( Constants.debugCalendarDataService && Constants.LOG_VERBOSE)
932 Log.v(TAG, "Received notification of Collections Table change.");
934 int id = -1;
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));
951 break;
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);
956 break;
957 case DatabaseChangedEvent.DATABASE_RECORD_INSERTED:
958 if (!exists) this.collectionCreated(cv);
959 else this.collectionUpdated(cv);
960 break;
961 default:
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 ) {
979 try {
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();
990 else {
991 this.changesDuringTx = true;
997 * Main worker thread execution loop
999 @Override
1000 public void run() {
1001 Log.i(TAG, "Main worker thread started.");
1003 setupCollections();
1005 if (Constants.LOG_DEBUG) Log.d(TAG, "Records added to queue. Starting main loop.");
1006 try {
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");
1011 updateAlarms();
1012 updateWidgets();
1014 if (callback != null) {
1015 try {
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();
1033 this.worker = null;
1034 this.stopSelf();
1036 else
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();
1050 resetWorker();
1055 /****************************************************
1056 * Private Classes *
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);
1097 @Override
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>();
1103 setupCollections();
1107 @Override
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);
1120 return events;
1123 @Override
1124 public boolean isInitialising() {
1125 return intialise;
1128 @Override
1129 public boolean isProcessing() {
1130 return processingNewData;
1133 public void registerCallback(DataRequestCallBack cb) {
1134 callback = cb;
1137 public void unregisterCallback(DataRequestCallBack cb) {
1138 callback = null;
1142 * Returns all the alarms that should have occurred between lastriggeredtime + 1 sec and now
1143 * @return
1144 * @throws RemoteException
1146 @Override
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.");
1155 ret = null;
1156 } else if (nextSnooze == null) {
1157 if (Constants.LOG_DEBUG)Log.d(TAG, "Only regular alarm in queue. Returning alarm: "+nextReg);
1158 ret = nextReg;
1159 } else if (nextReg == null) {
1160 if (Constants.LOG_DEBUG)Log.d(TAG, "Only snooze alarm in queue. Returning alarm: "+nextSnooze);
1161 ret = nextSnooze;
1162 } else {
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);
1168 ret = nextReg;
1169 } else {
1170 if (Constants.LOG_DEBUG)Log.d(TAG, "Next Snooze alarm has priority. Returning alarm: "+nextSnooze);
1171 ret = nextSnooze;
1175 if (ret == null) {
1176 setNextAlarmTrigger(); //calculate next trigger time
1177 return null;
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();
1184 return null;
1186 return ret;
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);
1196 saveState();
1202 @Override
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);
1221 break;
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);
1242 break;
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);
1253 break;
1255 default:
1256 throw new IllegalArgumentException("Invalid event action");
1259 try {
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.");
1269 @Override
1270 public void dismissAlarm(AcalAlarm alarm) throws RemoteException {
1271 this.triggeredAlarmDismissedByUser(alarm);
1275 @Override
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();
1285 saveState();
1289 private TodoList todoItems = new TodoList();
1291 @Override
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) );
1300 todoItems.sort();
1301 return todoItems.getList();
1304 @Override
1305 public SimpleAcalTodo getNthTodo(int n) throws RemoteException {
1306 return todoItems.getNth(n);
1309 @Override
1310 public int getNumberTodos() throws RemoteException {
1311 return todoItems.count();
1314 @Override
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());
1321 switch (action) {
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);
1331 break;
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);
1359 break;
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);
1370 break;
1372 default:
1373 throw new IllegalArgumentException("Invalid change action");
1376 try {
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.");