Fix final issues with timezone offsets in widget.
[acal.git] / src / com / morphoss / acal / davacal / AcalEvent.java
bloba329ce5f3bcf62b44b79ef0176f2c1990d6df752
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.davacal;
21 import java.io.Serializable;
22 import java.text.SimpleDateFormat;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.Map;
27 import android.content.ContentValues;
28 import android.content.Context;
29 import android.graphics.Color;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.util.Log;
34 import com.morphoss.acal.Constants;
35 import com.morphoss.acal.acaltime.AcalDateRange;
36 import com.morphoss.acal.acaltime.AcalDateTime;
37 import com.morphoss.acal.acaltime.AcalDuration;
38 import com.morphoss.acal.providers.DavResources;
39 import com.morphoss.acal.providers.PendingChanges;
41 /**
43 * @author Morphoss Ltd
46 public class AcalEvent implements Serializable, Parcelable, Comparable<AcalEvent>{
48 private static final long serialVersionUID = 1L;
49 public static final String TAG = "AcalEvent";
50 private AcalDateTime dtstart;
51 private AcalDateTime dtend;
52 private String summary;
53 private String description;
54 private String location;
55 private String repetition;
56 private int colour;
57 public final boolean hasAlarms;
58 public final int resourceId;
59 private List<AcalAlarm> alarmList = new ArrayList<AcalAlarm>();
60 private final String originalBlob;
61 private int collectionId;
62 public final boolean isPending;
63 private boolean alarmEnabled;
64 private int action = ACTION_CREATE;
65 private boolean dirty;
66 private final boolean[] dirtyFlags = new boolean[EVENT_FIELD.values().length];
68 public static final int ACTION_CREATE = 0;
69 public static final int ACTION_MODIFY_SINGLE = 1;
70 public static final int ACTION_MODIFY_ALL = 2;
71 public static final int ACTION_MODIFY_ALL_FUTURE = 3;
72 public static final int ACTION_DELETE_SINGLE = 4;
73 public static final int ACTION_DELETE_ALL = 5;
74 public static final int ACTION_DELETE_ALL_FUTURE = 6;
76 public static enum EVENT_FIELD {
77 resourceId,
78 startDate,
79 endDate,
80 summary,
81 location,
82 description,
83 colour,
84 collectionId,
85 repeatRule,
86 alarmList
89 public static final Parcelable.Creator<AcalEvent> CREATOR = new Parcelable.Creator<AcalEvent>() {
90 public AcalEvent createFromParcel(Parcel in) {
91 return getInstanceFromParcel(in);
94 public AcalEvent[] newArray(int size) {
95 return new AcalEvent[size];
99 public static AcalEvent getInstanceFromParcel(Parcel in) {
100 AcalEvent event = null;
101 event = new AcalEvent(in);
102 return event;
107 * Construct an AcalEvent from a row from the database. We optionally allow overwriting
108 * the start date/time for the event.
109 * @param context
110 * @param resourceId
111 * @return The AcalEvent we read, or null.
113 public static AcalEvent fromDatabase( Context context, int resourceId, AcalDateTime dtStart ) {
114 ContentValues resourceValues = DavResources.getRow(resourceId, context.getContentResolver());
115 ContentValues pendingValues = PendingChanges.getByResource(resourceId, context.getContentResolver());
116 if ( pendingValues != null ) {
117 String newData = pendingValues.getAsString(PendingChanges.NEW_DATA);
118 if ( newData == null ) newData = "";
119 resourceValues.put(DavResources.RESOURCE_DATA, newData);
120 resourceValues.put(DavResources.IS_PENDING, 1);
121 if ( Constants.LOG_DEBUG )
122 Log.i(TAG,"Using pending change record for AcalEvent fromDatabase.");
125 VComponent vc;
126 try {
127 vc = VComponent.createComponentFromResource( resourceValues,
128 AcalCollection.fromDatabase(context, resourceValues.getAsLong(DavResources.COLLECTION_ID)));
130 catch (VComponentCreationException e) {
131 Log.e(TAG,Log.getStackTraceString(e));
132 return null;
134 if ( ! (vc instanceof VCalendar) ) {
135 Log.w(TAG,"Trying to build AcalEvent but resource "+resourceId+" is not a VCalendar it is a "+vc.name );
136 return null;
138 Masterable event = ((VCalendar) vc).getMasterChild();
139 if ( ! (event instanceof VEvent) ) {
140 Log.w(TAG,"Trying to build AcalEvent but resource contained in "+resourceId+" is not a VEvent");
141 return null;
144 // This bit of mucking around preserves any timezone we got from the database
145 AcalDateTime eventStart = event.getStart();
146 AcalDateTime eventEnd = event.getEnd();
147 if ( dtStart != null ) {
148 if ( ! dtStart.equals(eventStart) ) {
149 long delta = dtStart.getEpoch() - eventStart.getEpoch();
150 eventStart.addSeconds(delta);
151 eventEnd.addSeconds(delta);
154 return new AcalEvent( (VEvent) event, eventStart, eventEnd, false );
157 @Override
158 public int describeContents() {
159 return 0;
161 @Override
162 public void writeToParcel(Parcel out, int flags) {
163 getStart().writeToParcel( out, flags);
164 getEnd().writeToParcel(out, 0);
165 out.writeString(getSummary());
166 out.writeString(getLocation());
167 out.writeString(getDescription());
168 out.writeString(getRepetition());
169 out.writeInt(getColour());
170 out.writeByte((byte) (hasAlarms() ? 'T' : 'F'));
171 out.writeByte((byte) (alarmEnabled ? 'T' : 'F'));
172 out.writeInt(getResourceId());
173 out.writeString(originalBlob);
174 out.writeInt(collectionId);
175 out.writeTypedList(alarmList);
176 out.writeByte((byte) (isPending ? 'T' : 'F'));
179 public AcalEvent(Parcel in) {
180 this.dtstart = AcalDateTime.unwrapParcel(in);
181 this.dtend = AcalDateTime.unwrapParcel(in);
182 this.summary = in.readString();
183 this.location = in.readString();
184 this.description = in.readString();
185 this.repetition = in.readString();
186 this.colour = in.readInt();
187 this.hasAlarms = in.readByte() == 'T';
188 this.alarmEnabled = in.readByte() == 'T';
189 this.resourceId = in.readInt();
190 this.originalBlob = in.readString();
191 this.collectionId = in.readInt();
192 in.readTypedList(this.alarmList, AcalAlarm.CREATOR);
193 this.isPending = in.readByte() == 'T';
196 @Override
197 public int compareTo(AcalEvent other) {
198 if ( getLocalStart().before(other.getLocalStart())) return -1;
199 if ( getLocalStart().after(other.getLocalStart())) return 1;
200 if ( getLocalEnd().before(other.getLocalEnd())) return -1;
201 if ( getLocalEnd().after(other.getLocalEnd())) return 1;
202 return 0;
205 public AcalEvent(VEvent event, AcalDateTime startDate, AcalDateTime endDate, boolean isPending ) {
206 this.resourceId = event.getResourceId();
207 this.dtstart = startDate;
208 this.dtend = endDate;
209 this.summary = safeEventPropertyValue(event, PropertyName.SUMMARY);
210 this.description = safeEventPropertyValue(event, PropertyName.DESCRIPTION);
211 this.location = safeEventPropertyValue(event, PropertyName.LOCATION);
212 this.originalBlob = event.getTopParent().getOriginalBlob();
213 String repeatRule = safeEventPropertyValue(event,PropertyName.RRULE);
214 String repeatInstances = safeEventPropertyValue(event,PropertyName.RDATE);
215 this.repetition = repeatRule + (repeatInstances.equals("")?"":"\n"+repeatInstances);
217 List<AcalAlarm> theseAlarms = new ArrayList<AcalAlarm>();
218 for( VComponent child : event.getChildren() ) {
219 if ( child instanceof VAlarm ) {
220 theseAlarms.add( new AcalAlarm((VAlarm) child, (Masterable) event,
221 (dtstart == null ? null : dtstart.clone()),
222 (dtend == null ? null : dtend.clone()) ) );
226 alarmList.addAll(theseAlarms);
227 hasAlarms = alarmList.size() > 0;
229 int aColor = Color.BLUE;
230 boolean alarmsForCollection = true;
231 try {
232 aColor = event.getCollectionColour();
233 alarmsForCollection = event.getAlarmEnabled();
234 } catch (Exception e) {
235 Log.e(TAG,"Error Creating AcalEvent - "+e.getMessage());
236 Log.e(TAG,Log.getStackTraceString(e));
238 colour = aColor;
239 alarmEnabled = alarmsForCollection;
241 int fromCollectionId;
242 try {
243 fromCollectionId = event.getCollectionId();
245 catch( NullPointerException e ) {
246 fromCollectionId = -1;
248 collectionId = fromCollectionId;
249 this.isPending = isPending;
253 private String safeEventPropertyValue(VComponent event, PropertyName summary2 ) {
254 String propertyValue = null;
255 try {
256 propertyValue = event.getProperty(summary2).getValue();
258 catch (Exception e) {
260 if (propertyValue == null) propertyValue = "";
261 return propertyValue;
267 public List<AcalAlarm> getAlarms() {
268 return this.alarmList;
271 public int getColour() {
272 return this.colour;
275 public boolean getAlarmEnabled() {
276 return this.alarmEnabled;
279 public String getDescription() {
280 return this.description;
283 public AcalDateTime getEnd() {
284 return this.dtend.clone();
287 public AcalDuration getDuration() {
288 return dtstart.getDurationTo(dtend);
291 public String getLocation() {
292 return this.location;
295 public String getRepetition() {
296 return this.repetition;
299 public void setRepetition(String newRepetition) {
300 this.repetition = newRepetition;
301 this.dirty = true;
304 @SuppressWarnings("unchecked")
305 public void setField(EVENT_FIELD field, Object val) {
306 switch( field ) {
307 case startDate:
308 this.dtstart = ((AcalDateTime) val).clone();
309 break;
310 case endDate:
311 this.dtend = ((AcalDateTime) val).clone();
312 break;
313 case summary:
314 this.summary = (String) val;
315 break;
316 case description:
317 this.description = (String) val;
318 break;
319 case location:
320 this.location = (String) val;
321 break;
322 case repeatRule:
323 this.repetition = (String) val;
324 break;
325 case collectionId:
326 this.collectionId = (Integer) val;
327 break;
328 case colour:
329 this.colour = (Integer) val;
330 break;
331 case alarmList:
332 this.alarmList = (List<AcalAlarm>) val;
333 break;
334 default:
335 throw new IllegalStateException("The "+field.toString()+" is not modifiable.");
337 dirtyFlags[field.ordinal()] = true;
338 dirty=true;
341 public AcalDateTime getStart() {
342 return this.dtstart.clone();
345 public AcalDateTime getLocalStart() {
346 return this.getStart().applyLocalTimeZone();
349 public AcalDateTime getLocalEnd() {
350 AcalDateTime finish = getEnd();
351 if ( finish != null ) finish.applyLocalTimeZone();
352 return finish;
355 public String getTimeText(AcalDateTime viewDateStart, AcalDateTime viewDateEnd, boolean as24HourTime ) {
356 AcalDateTime start = this.getLocalStart();
357 AcalDateTime finish = this.getLocalEnd();
358 String timeText = "";
359 String timeFormatString = (as24HourTime ? "HH:mm" : "hh:mmaa");
360 SimpleDateFormat timeFormatter = new SimpleDateFormat(timeFormatString);
362 if ( start.before(viewDateStart) || (finish != null && finish.after(viewDateEnd)) ){
363 if ( start.isDate() ) {
364 timeText = AcalDateTime.fmtDayMonthYear(start)+ ", all day";
366 else {
367 SimpleDateFormat startFormatter = timeFormatter;
368 SimpleDateFormat finishFormatter = timeFormatter;
370 if ( start.before(viewDateStart) || start.after(viewDateEnd) ) {
371 startFormatter = new SimpleDateFormat("MMM d, "+timeFormatString);
372 if ( (finish.getYear() > start.getYear()) || (finish.getYearDay() > start.getYearDay()) )
373 finishFormatter = new SimpleDateFormat("MMM d, "+timeFormatString);
376 timeText = (startFormatter.format(start.toJavaDate())+" - "
377 + (finish == null ? "null" : finishFormatter.format(finish.toJavaDate())));
380 else if ( start.isDate()) {
381 timeText = "All Day";
383 else {
384 timeText = (timeFormatter.format(start.toJavaDate())+" - "
385 + (finish == null ? "null" : timeFormatter.format(finish.toJavaDate())));
387 return timeText;
390 public boolean overlaps( AcalDateRange range ) {
391 return range.overlaps(dtstart, dtend);
395 * Get the value from the SUMMARY field.
396 * @return
398 public String getSummary() {
399 return this.summary;
403 * Ascertain whether the event has alarms.
404 * @return true, if the event has alarms
406 public boolean hasAlarms() {
407 return this.hasAlarms;
410 public int getResourceId() {
411 return this.resourceId;
414 public int getCollectionId() {
415 return this.collectionId;
418 public String getOriginalBlob() {
419 return this.originalBlob;
422 public void setAction(int action) {
423 this.action = action;
426 public boolean isDirty() { return dirty; }
427 public int getAction() {
428 return this.action;
431 public boolean isModifyAction() {
432 return (this.action == ACTION_MODIFY_SINGLE) ||
433 (this.action == ACTION_MODIFY_ALL) ||
434 (this.action == ACTION_MODIFY_ALL_FUTURE);
438 public AcalEvent(Map<EVENT_FIELD, Object> defaults) {
439 this.dtstart = (AcalDateTime) defaults.get(EVENT_FIELD.startDate);
440 this.dtend = (AcalDateTime) defaults.get(EVENT_FIELD.endDate);
441 this.summary = (String) defaults.get(EVENT_FIELD.summary);
442 this.description = (String) defaults.get(EVENT_FIELD.description);
443 this.location = (String) defaults.get(EVENT_FIELD.location);
444 this.repetition = (String) defaults.get(EVENT_FIELD.repeatRule);
445 this.colour = (Integer) defaults.get(EVENT_FIELD.colour);
446 @SuppressWarnings("unchecked")
447 List<AcalAlarm> alarmList = (List<AcalAlarm>) defaults.get(EVENT_FIELD.alarmList);
448 this.hasAlarms = ! alarmList.isEmpty();
449 this.alarmList.addAll(alarmList);
451 int theId = -1;
452 if ( (Integer) defaults.get(EVENT_FIELD.resourceId) != null )
453 theId = (Integer) defaults.get(EVENT_FIELD.resourceId);
454 this.resourceId = theId;
456 theId = -1;
457 if ( (Integer) defaults.get(EVENT_FIELD.collectionId) != null )
458 theId = (Integer) defaults.get(EVENT_FIELD.collectionId);
459 this.collectionId = theId;
461 this.originalBlob = null;
462 this.isPending = false;
463 this.alarmEnabled = true;
464 this.dirty = false;
468 public AcalEvent(VEvent vEvent, AcalDateTime dtstart, AcalDuration duration, boolean isPending) {
469 this(vEvent, dtstart, dtstart.clone().addDuration(duration), isPending);