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
.davacal
;
21 import java
.util
.ArrayList
;
22 import java
.util
.Collections
;
23 import java
.util
.HashSet
;
24 import java
.util
.List
;
25 import java
.util
.TimeZone
;
26 import java
.util
.regex
.Matcher
;
28 import android
.os
.Parcel
;
29 import android
.os
.Parcelable
;
30 import android
.util
.Log
;
32 import com
.morphoss
.acal
.Constants
;
33 import com
.morphoss
.acal
.acaltime
.AcalDateRange
;
34 import com
.morphoss
.acal
.acaltime
.AcalDateTime
;
35 import com
.morphoss
.acal
.acaltime
.AcalRepeatRule
;
36 import com
.morphoss
.acal
.acaltime
.AcalRepeatRuleParser
;
38 public class VCalendar
extends VComponent
{
39 public static final String TAG
= "aCal VCalendar";
40 private AcalDateRange dateRange
= null;
41 private AcalRepeatRule repeatRule
= null;
42 private Boolean masterHasOverrides
= null;
43 private Boolean hasAlarms
= null;
44 private Boolean hasRepeatRule
= null;
45 private Long earliestStart
;
46 private Long latestEnd
;
47 private boolean isPending
= false;
50 public VCalendar(ComponentParts splitter
, Integer resourceId
, Long earliestStart
, Long latestEnd
, AcalCollection collectionObject
,VComponent parent
) {
51 super(splitter
, resourceId
, collectionObject
,parent
);
52 this.earliestStart
= earliestStart
;
53 this.latestEnd
= latestEnd
;
54 if ( earliestStart
!= null ) {
55 this.dateRange
= new AcalDateRange(AcalDateTime
.fromMillis(earliestStart
),
56 (latestEnd
== null ?
null : AcalDateTime
.fromMillis(latestEnd
)));
61 protected VCalendar(AcalCollection collection
) {
62 super( VComponent
.VCALENDAR
, collection
, null );
63 try { setPersistentOn(); } catch (YouMustSurroundThisMethodInTryCatchOrIllEatYouException e
) { }
64 addProperty(new AcalProperty("CALSCALE","GREGORIAN"));
65 addProperty(new AcalProperty("PRODID","-//morphoss.com//aCal 1.0//EN"));
66 addProperty(new AcalProperty("VERSION","2.0"));
70 public static VCalendar
getGenericCalendar( AcalCollection collection
, AcalEvent newEventData
) {
71 VCalendar vcal
= new VCalendar(collection
);
76 public void setPending(boolean isPending
) {
77 this.isPending
= isPending
;
79 public boolean isPending() {
80 return this.isPending
;
83 public VCalendar
clone() {
84 return new VCalendar(this.content
, this.resourceId
, this.earliestStart
, this.latestEnd
, this.collectionData
, this.parent
);
87 public String
applyEventAction(AcalEvent action
) {
91 VEvent vEvent
= (VEvent
) this.getMasterChild();
93 // first, strip any existing properties which we always modify
94 vEvent
.removeProperties( new PropertyName
[] {PropertyName
.DTSTAMP
, PropertyName
.LAST_MODIFIED
} );
97 AcalDateTime lastModified
= new AcalDateTime();
98 lastModified
.setTimeZone(TimeZone
.getDefault().getID());
99 lastModified
.shiftTimeZone("UTC");
101 vEvent
.addProperty(AcalProperty
.fromString(lastModified
.toPropertyString(PropertyName
.DTSTAMP
)));
102 vEvent
.addProperty(AcalProperty
.fromString(lastModified
.toPropertyString(PropertyName
.LAST_MODIFIED
)));
104 if ( action
.getAction() == AcalEvent
.ACTION_DELETE_SINGLE
) {
105 AcalProperty exDate
= vEvent
.getProperty("EXDATE");
106 if ( exDate
== null || exDate
.getValue().equals("") )
107 exDate
= AcalProperty
.fromString(action
.getStart().toPropertyString(PropertyName
.EXDATE
));
109 vEvent
.removeProperties( new PropertyName
[] {PropertyName
.EXDATE
} );
110 exDate
= AcalProperty
.fromString(exDate
.toRfcString() + "," + action
.getStart().fmtIcal() );
112 vEvent
.addProperty(exDate
);
114 else if (action
.getAction() == AcalEvent
.ACTION_DELETE_ALL_FUTURE
) {
115 AcalRepeatRuleParser parsedRule
= AcalRepeatRuleParser
.parseRepeatRule(action
.getRepetition());
116 AcalDateTime until
= action
.getStart().clone();
117 until
.addSeconds(-1);
118 parsedRule
.setUntil(until
);
119 String rrule
= parsedRule
.toString();
120 action
.setRepetition(rrule
);
121 vEvent
.removeProperties( new PropertyName
[] {PropertyName
.RRULE
} );
122 vEvent
.addProperty(new AcalProperty("RRULE",rrule
));
124 else if (action
.isModifyAction()) {
125 this.applyModify(vEvent
,action
);
126 this.updateTimeZones(vEvent
);
129 String ret
= this.getCurrentBlob();
132 catch (Exception e
) {
133 Log
.w(TAG
,Log
.getStackTraceString(e
));
139 private void applyModify(Masterable mast
, AcalEvent action
) {
140 //there are 3 possible modify actions:
141 if (action
.getAction() == AcalEvent
.ACTION_MODIFY_SINGLE
) {
142 // Only modify the single instance
144 else if (action
.getAction() == AcalEvent
.ACTION_MODIFY_ALL_FUTURE
) {
145 // Modify this instance, and all future instances.
148 else if (action
.getAction() == AcalEvent
.ACTION_MODIFY_ALL
) {
149 // Modify all instances
151 // First, strip any existing properties which we modify
152 mast
.removeProperties( new PropertyName
[] {PropertyName
.DTSTART
, PropertyName
.DTEND
, PropertyName
.DURATION
,
153 PropertyName
.SUMMARY
, PropertyName
.LOCATION
, PropertyName
.DESCRIPTION
, PropertyName
.RRULE
} );
155 AcalDateTime dtStart
= action
.getStart();
156 mast
.addProperty( dtStart
.asProperty(PropertyName
.DTSTART
));
158 AcalDateTime dtEnd
= action
.getEnd();
159 if ( (dtEnd
.getTimeZoneId() == null && dtStart
.getTimeZoneId() == null) ||
160 (dtEnd
.getTimeZoneId() != null && dtEnd
.getTimeZoneId().equals(dtStart
.getTimeZoneId())) )
161 mast
.addProperty(action
.getDuration().asProperty(PropertyName
.DURATION
) );
163 mast
.addProperty( dtEnd
.asProperty( PropertyName
.DTEND
) );
165 mast
.addProperty(new AcalProperty(PropertyName
.SUMMARY
, action
.getSummary()));
167 String location
= action
.getLocation();
168 if ( !location
.equals("") )
169 mast
.addProperty(new AcalProperty(PropertyName
.LOCATION
,location
));
171 String description
= action
.getDescription();
172 if ( !description
.equals("") )
173 mast
.addProperty(new AcalProperty(PropertyName
.DESCRIPTION
,description
));
175 String rrule
= action
.getRepetition();
176 if ( rrule
!= null && !rrule
.equals(""))
177 mast
.addProperty(new AcalProperty(PropertyName
.RRULE
,rrule
));
179 mast
.updateAlarmComponents( action
.getAlarms() );
183 private void updateTimeZones(VEvent vEvent
) {
184 HashSet
<String
> tzIdSet
= new HashSet
<String
>();
185 for( PropertyName pn
: PropertyName
.localisableDateProperties() ) {
186 AcalProperty p
= vEvent
.getProperty(pn
);
188 String tzId
= p
.getParam("TZID");
189 if ( tzId
!= null ) {
190 tzIdSet
.add(p
.getParam("TZID"));
191 if ( Constants
.LOG_DEBUG
)
192 Log
.println(Constants
.LOGD
,TAG
,"Found reference to timezone '"+tzId
+"' in event.");
197 List
<VComponent
> removeChildren
= new ArrayList
<VComponent
>();
198 for (VComponent child
: getChildren() ) {
199 if ( child
.name
.equals(VComponent
.VTIMEZONE
) ) {
202 VTimezone vtz
= (VTimezone
) child
;
203 tzId
= vtz
.getTZID();
205 catch( Exception e
) {};
206 if ( tzIdSet
.contains(tzId
) ) {
207 if ( Constants
.LOG_DEBUG
)
208 Log
.println(Constants
.LOGD
,TAG
,"Found child vtimezone for '"+tzId
+"' in event.");
209 tzIdSet
.remove(tzId
);
212 if ( Constants
.LOG_DEBUG
)
213 Log
.println(Constants
.LOGD
,TAG
,"Removing vtimezone for '"+tzId
+"' from event.");
214 removeChildren
.add(child
);
218 if ( Constants
.LOG_DEBUG
)
219 Log
.println(Constants
.LOGD
,TAG
,"Found "+child
.name
+" component in event.");
222 // Have to avoid the concurrent modification
223 for(VComponent child
: removeChildren
) {
224 this.removeChild(child
);
227 for ( String tzId
: tzIdSet
) {
230 String tzBlob
= VTimezone
.getZoneDefinition(tzId
);
231 if ( Constants
.LOG_DEBUG
) {
232 Log
.println(Constants
.LOGD
,TAG
,"New timezone for '"+tzId
+"'");
233 Log
.println(Constants
.LOGD
,TAG
,tzBlob
);
235 vtz
= (VTimezone
) VComponent
.createComponentFromBlob(tzBlob
, null, collectionData
);
239 catch ( UnrecognizedTimeZoneException e
) {
240 Log
.i(TAG
,"Unable to build a timezone for '"+tzId
+"'");
247 public void checkRepeatRule() {
249 if (repeatRule
== null) repeatRule
= AcalRepeatRule
.fromVCalendar(this);
251 catch ( Exception e
) {
252 Log
.e(TAG
,"Exception getting repeat rule from VCalendar", e
);
254 hasRepeatRule
= ( repeatRule
!= null );
257 public boolean appendAlarmInstancesBetween(List
<AcalAlarm
> alarmList
, AcalDateRange rangeRequested
) {
258 if ( hasRepeatRule
== null && repeatRule
== null ) checkRepeatRule();
259 if ( !hasRepeatRule
) return false;
260 this.repeatRule
.appendAlarmInstancesBetween(alarmList
, rangeRequested
);
264 public boolean appendEventInstancesBetween(List
<AcalEvent
> eventList
, AcalDateRange rangeRequested
, boolean isPending
) {
266 this.isPending
= true;
267 this.repeatRule
= null;
270 if (dateRange
!= null) {
271 AcalDateRange intersection
= rangeRequested
.getIntersection(this.dateRange
);
272 if (intersection
!= null) {
273 if (hasRepeatRule
== null && repeatRule
== null) checkRepeatRule();
275 // Log.d(TAG,"Processing event: Summary="+new UnModifiableAcalEvent(thisEvent,new AcalCalendar(), new AcalCalendar()).summary);
276 this.repeatRule
.appendEventsInstancesBetween(eventList
, intersection
);
283 if (repeatRule
!= null) {
284 this.repeatRule
.appendEventsInstancesBetween(eventList
, rangeRequested
);
288 // Log.d(TAG,"Skipped event: Summary="+new UnModifiableAcalEvent(thisEvent,new AcalCalendar(), new AcalCalendar()).summary);
291 if (Constants
.LOG_DEBUG
)Log
.d(TAG
,"Exception in RepeatRule handling");
292 if (Constants
.LOG_DEBUG
)Log
.d(TAG
,Log
.getStackTraceString(e
));
297 public Masterable
getMasterChild() {
299 for (VComponent vc
: this.getChildren()) {
300 if ( vc
instanceof VEvent
) return (VEvent
)vc
;
301 if ( vc
instanceof VTodo
) return (VTodo
) vc
;
304 for( PartInfo childInfo
: content
.partInfo
) {
305 if ( childInfo
.type
.equals(VEVENT
) ) {
306 return new VEvent(new ComponentParts(childInfo
.getComponent(content
.componentString
)),
307 resourceId
, collectionData
,this);
309 else if ( childInfo
.type
.equals(VTODO
) ) {
310 return new VTodo(new ComponentParts(childInfo
.getComponent(content
.componentString
)),
311 resourceId
, collectionData
,this);
317 public AcalDateTime
getRangeEnd() {
318 if ( dateRange
== null ) return null;
319 return dateRange
.end
;
322 public boolean masterHasOverrides() {
323 if ( masterHasOverrides
== null ) {
324 int countMasterables
= 0;
325 for( PartInfo childInfo
: content
.partInfo
) {
326 if ( childInfo
.type
.equals(VEVENT
) || childInfo
.type
.equals(VTODO
) ) {
328 if ( countMasterables
> 1 ) break;
331 if ( masterHasOverrides
== null ) masterHasOverrides
= (countMasterables
> 1);
333 return masterHasOverrides
;
337 public Masterable
getChildFromRecurrenceId(RecurrenceId recurrenceProperty
) {
338 if ( masterHasOverrides() ) return this.getMasterChild();
340 this.setPersistentOn();
341 List
<Masterable
> matchingChildren
= new ArrayList
<Masterable
>();
342 for (VComponent vc
: this.getChildren()) {
343 if (vc
.containsPropertyKey(recurrenceProperty
.getName()) && vc
instanceof Masterable
)
344 matchingChildren
.add((Masterable
) vc
);
346 if (matchingChildren
.isEmpty()) {
347 // Won't happen since we test for this in masterHasOverrides()
348 return this.getMasterChild();
350 Collections
.sort(matchingChildren
, RecurrenceId
.getVComponentComparatorByRecurrenceId());
351 for (int i
= matchingChildren
.size()-1; i
>= 0; i
--) {
352 RecurrenceId cur
= (RecurrenceId
) matchingChildren
.get(i
).getProperty("RECURRENCE-ID");
353 if (cur
.notAfter(recurrenceProperty
)) {
354 this.setPersistentOff();
355 return matchingChildren
.get(i
);
358 } catch (YouMustSurroundThisMethodInTryCatchOrIllEatYouException e
) {
359 Log
.w(TAG
,Log
.getStackTraceString(e
));
361 this.setPersistentOff();
364 return this.getMasterChild();
368 public String
getOlsonName( String TzID
) {
369 Matcher m
= Constants
.tzOlsonExtractor
.matcher(TzID
);
375 for (VComponent vc
: this.getChildren()) {
376 if ( vc
instanceof VTimezone
) {
377 if ( vc
.getProperty("TZID").getValue() == TzID
) {
378 AcalProperty idProperty
= vc
.getProperty("X-MICROSOFT-CDO-TZID");
379 if ( idProperty
!= null && idProperty
.getValue() != null ) {
380 switch( Integer
.parseInt(idProperty
.getValue()) ) {
381 case 0: return("UTC");
382 case 1: return("Europe/London");
383 case 2: return("Europe/Lisbon");
384 case 3: return("Europe/Paris");
385 case 4: return("Europe/Berlin");
386 case 5: return("Europe/Bucharest");
387 case 6: return("Europe/Prague");
388 case 7: return("Europe/Athens");
389 case 8: return("America/Brasilia");
390 case 9: return("America/Halifax");
391 case 10: return("America/New_York");
392 case 11: return("America/Chicago");
393 case 12: return("America/Denver");
394 case 13: return("America/Los_Angeles");
395 case 14: return("America/Anchorage");
396 case 15: return("Pacific/Honolulu");
397 case 16: return("Pacific/Apia");
398 case 17: return("Pacific/Auckland");
399 case 18: return("Australia/Brisbane");
400 case 19: return("Australia/Adelaide");
401 case 20: return("Asia/Tokyo");
402 case 21: return("Asia/Singapore");
403 case 22: return("Asia/Bangkok");
404 case 23: return("Asia/Kolkata");
405 case 24: return("Asia/Muscat");
406 case 25: return("Asia/Tehran");
407 case 26: return("Asia/Baghdad");
408 case 27: return("Asia/Jerusalem");
409 case 28: return("America/St_Johns");
410 case 29: return("Atlantic/Azores");
411 case 30: return("America/Noronha");
412 case 31: return("Africa/Casablanca");
413 case 32: return("America/Argentina/Buenos_Aires");
414 case 33: return("America/La_Paz");
415 case 34: return("America/Indiana/Indianapolis");
416 case 35: return("America/Bogota");
417 case 36: return("America/Regina");
418 case 37: return("America/Tegucigalpa");
419 case 38: return("America/Phoenix");
420 case 39: return("Pacific/Kwajalein");
421 case 40: return("Pacific/Fiji");
422 case 41: return("Asia/Magadan");
423 case 42: return("Australia/Hobart");
424 case 43: return("Pacific/Guam");
425 case 44: return("Australia/Darwin");
426 case 45: return("Asia/Shanghai");
427 case 46: return("Asia/Novosibirsk");
428 case 47: return("Asia/Karachi");
429 case 48: return("Asia/Kabul");
430 case 49: return("Africa/Cairo");
431 case 50: return("Africa/Harare");
432 case 51: return("Europe/Moscow");
433 case 53: return("Atlantic/Cape_Verde");
434 case 54: return("Asia/Yerevan");
435 case 55: return("America/Panama");
436 case 56: return("Africa/Nairobi");
437 case 58: return("Asia/Yekaterinburg");
438 case 59: return("Europe/Helsinki");
439 case 60: return("America/Godthab");
440 case 61: return("Asia/Rangoon");
441 case 62: return("Asia/Kathmandu");
442 case 63: return("Asia/Irkutsk");
443 case 64: return("Asia/Krasnoyarsk");
444 case 65: return("America/Santiago");
445 case 66: return("Asia/Colombo");
446 case 67: return("Pacific/Tongatapu");
447 case 68: return("Asia/Vladivostok");
448 case 69: return("Africa/Ndjamena");
449 case 70: return("Asia/Yakutsk");
450 case 71: return("Asia/Dhaka");
451 case 72: return("Asia/Seoul");
452 case 73: return("Australia/Perth");
453 case 74: return("Asia/Riyadh");
454 case 75: return("Asia/Taipei");
455 case 76: return("Australia/Sydney");
469 return null; // We failed :-(
473 public boolean hasAlarm() {
474 if ( this.hasAlarms
!= null ) return this.hasAlarms
;
475 for( PartInfo childInfo
: content
.partInfo
) {
476 if ( childInfo
.type
.equals(VEVENT
) ) {
477 VEvent vc
= new VEvent(new ComponentParts(childInfo
.getComponent(content
.componentString
)),
478 resourceId
, collectionData
,this);
479 for( PartInfo childChildInfo
: vc
.content
.partInfo
) {
480 if ( childChildInfo
.type
.equals(VALARM
)) {
481 this.hasAlarms
= true;
486 else if ( childInfo
.type
.equals(VTODO
) ) {
487 VTodo vc
= new VTodo(new ComponentParts(childInfo
.getComponent(content
.componentString
)),
488 resourceId
, collectionData
,this);
489 for( PartInfo childChildInfo
: vc
.content
.partInfo
) {
490 if ( childChildInfo
.type
.equals(VALARM
)) {
491 this.hasAlarms
= true;
496 else if ( childInfo
.type
.equals(VALARM
)) {
497 this.hasAlarms
= true;
501 this.hasAlarms
= false;
506 public VCalendar(Parcel in
) {
510 public static final Parcelable
.Creator
<VCalendar
> CREATOR
= new Parcelable
.Creator
<VCalendar
>() {
511 public VCalendar
createFromParcel(Parcel in
) {
512 return new VCalendar(in
);
515 public VCalendar
[] newArray(int size
) {
516 return new VCalendar
[size
];