Code drop from //branches/cupcake/...@124589
[android/platform/packages/apps/Calendar.git] / src / com / android / calendar / DeleteEventHelper.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.calendar;
18
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.ContentValues;
24 import android.content.DialogInterface;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.pim.EventRecurrence;
28 import android.provider.Calendar;
29 import android.provider.Calendar.Events;
30 import android.text.format.Time;
31
32 /**
33  * A helper class for deleting events.  If a normal event is selected for
34  * deletion, then this pops up a confirmation dialog.  If the user confirms,
35  * then the normal event is deleted.
36  * 
37  * <p>
38  * If a repeating event is selected for deletion, then this pops up dialog
39  * asking if the user wants to delete just this one instance, or all the
40  * events in the series, or this event plus all following events.  The user
41  * may also cancel the delete.
42  * </p>
43  * 
44  * <p>
45  * To use this class, create an instance, passing in the parent activity
46  * and a boolean that determines if the parent activity should exit if the
47  * event is deleted.  Then to use the instance, call one of the
48  * {@link delete()} methods on this class.
49  * 
50  * An instance of this class may be created once and reused (by calling
51  * {@link #delete()} multiple times).
52  */
53 public class DeleteEventHelper {
54     
55     private static final String TAG = "DeleteEventHelper";
56     private final Activity mParent;
57     private final ContentResolver mContentResolver;
58     
59     private long mStartMillis;
60     private long mEndMillis;
61     private Cursor mCursor;
62     
63     /**
64      * If true, then call finish() on the parent activity when done.
65      */
66     private boolean mExitWhenDone;
67     
68     /**
69      * These are the corresponding indices into the array of strings
70      * "R.array.delete_repeating_labels" in the resource file.
71      */
72     static final int DELETE_SELECTED = 0;
73     static final int DELETE_ALL_FOLLOWING = 1;
74     static final int DELETE_ALL = 2;
75     
76     private int mWhichDelete;
77
78     private static final String[] EVENT_PROJECTION = new String[] {
79         Events._ID,
80         Events.TITLE,
81         Events.ALL_DAY,
82         Events.CALENDAR_ID,
83         Events.RRULE,
84         Events.DTSTART,
85         Events._SYNC_ID,
86         Events.EVENT_TIMEZONE,
87     };
88
89     private int mEventIndexId;
90     private int mEventIndexRrule;
91     private String mSyncId;
92     
93     public DeleteEventHelper(Activity parent, boolean exitWhenDone) {
94         mParent = parent;
95         mContentResolver = mParent.getContentResolver();
96         mExitWhenDone = exitWhenDone;
97     }
98     
99     public void setExitWhenDone(boolean exitWhenDone) {
100         mExitWhenDone = exitWhenDone;
101     }
102
103     /**
104      * This callback is used when a normal event is deleted.
105      */
106     private DialogInterface.OnClickListener mDeleteNormalDialogListener =
107             new DialogInterface.OnClickListener() {
108         public void onClick(DialogInterface dialog, int button) {
109             long id = mCursor.getInt(mEventIndexId);
110             Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
111             mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
112             if (mExitWhenDone) {
113                 mParent.finish();
114             }
115         }
116     };
117
118     /**
119      * This callback is used when a list item for a repeating event is selected
120      */
121     private DialogInterface.OnClickListener mDeleteListListener =
122             new DialogInterface.OnClickListener() {
123         public void onClick(DialogInterface dialog, int button) {
124             mWhichDelete = button;
125         }
126     };
127
128     /**
129      * This callback is used when a repeating event is deleted.
130      */
131     private DialogInterface.OnClickListener mDeleteRepeatingDialogListener =
132             new DialogInterface.OnClickListener() {
133         public void onClick(DialogInterface dialog, int button) {
134             if (mWhichDelete != -1) {
135                 deleteRepeatingEvent(mWhichDelete);
136             }
137         }
138     };
139     
140     /**
141      * Does the required processing for deleting an event, which includes
142      * first popping up a dialog asking for confirmation (if the event is
143      * a normal event) or a dialog asking which events to delete (if the
144      * event is a repeating event).  The "which" parameter is used to check
145      * the initial selection and is only used for repeating events.  Set
146      * "which" to -1 to have nothing selected initially.
147      * 
148      * @param begin the begin time of the event, in UTC milliseconds
149      * @param end the end time of the event, in UTC milliseconds
150      * @param eventId the event id
151      * @param which one of the values {@link DELETE_SELECTED},
152      *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
153      */
154     public void delete(long begin, long end, long eventId, int which) {
155         Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId);
156         Cursor cursor = mParent.managedQuery(uri, EVENT_PROJECTION, null, null);
157         if (cursor == null) {
158             return;
159         }
160         cursor.moveToFirst();
161         delete(begin, end, cursor, which);
162     }
163     
164     /**
165      * Does the required processing for deleting an event.  This method
166      * takes a {@link Cursor} object as a parameter, which must point to
167      * a row in the Events table containing the required database fields.
168      * The required fields for a normal event are:
169      * 
170      * <ul>
171      *   <li> Events._ID </li>
172      *   <li> Events.TITLE </li>
173      *   <li> Events.RRULE </li>
174      * </ul>
175      * 
176      * The required fields for a repeating event include the above plus the
177      * following fields:
178      * 
179      * <ul>
180      *   <li> Events.ALL_DAY </li>
181      *   <li> Events.CALENDAR_ID </li>
182      *   <li> Events.DTSTART </li>
183      *   <li> Events._SYNC_ID </li>
184      *   <li> Events.EVENT_TIMEZONE </li>
185      * </ul>
186      * 
187      * @param begin the begin time of the event, in UTC milliseconds
188      * @param end the end time of the event, in UTC milliseconds
189      * @param cursor the database cursor containing the required fields
190      * @param which one of the values {@link DELETE_SELECTED},
191      *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
192      */
193     public void delete(long begin, long end, Cursor cursor, int which) {
194         mWhichDelete = which;
195         mStartMillis = begin;
196         mEndMillis = end;
197         mCursor = cursor;
198         mEventIndexId = mCursor.getColumnIndexOrThrow(Events._ID);
199         mEventIndexRrule = mCursor.getColumnIndexOrThrow(Events.RRULE);
200         int eventIndexSyncId = mCursor.getColumnIndexOrThrow(Events._SYNC_ID);
201         mSyncId = mCursor.getString(eventIndexSyncId);
202         
203         // If this is a repeating event, then pop up a dialog asking the
204         // user if they want to delete all of the repeating events or
205         // just some of them.
206         String rRule = mCursor.getString(mEventIndexRrule);
207         if (rRule == null) {
208             // This is a normal event. Pop up a confirmation dialog.
209             new AlertDialog.Builder(mParent)
210             .setTitle(R.string.delete_title)
211             .setMessage(R.string.delete_this_event_title)
212             .setIcon(android.R.drawable.ic_dialog_alert)
213             .setPositiveButton(android.R.string.ok, mDeleteNormalDialogListener)
214             .setNegativeButton(android.R.string.cancel, null)
215             .show();
216         } else {
217             // This is a repeating event.  Pop up a dialog asking which events
218             // to delete.
219             int labelsArrayId = R.array.delete_repeating_labels;
220             if (mSyncId == null) {
221                 labelsArrayId = R.array.delete_repeating_labels_no_selected;
222             }
223             new AlertDialog.Builder(mParent)
224             .setTitle(R.string.delete_title)
225             .setIcon(android.R.drawable.ic_dialog_alert)
226             .setSingleChoiceItems(labelsArrayId, which, mDeleteListListener)
227             .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
228             .setNegativeButton(android.R.string.cancel, null)
229             .show();
230         }
231     }
232     
233     private void deleteRepeatingEvent(int which) {
234         int indexDtstart = mCursor.getColumnIndexOrThrow(Events.DTSTART);
235         int indexAllDay = mCursor.getColumnIndexOrThrow(Events.ALL_DAY);
236         int indexTitle = mCursor.getColumnIndexOrThrow(Events.TITLE);
237         int indexTimezone = mCursor.getColumnIndexOrThrow(Events.EVENT_TIMEZONE);
238         int indexCalendarId = mCursor.getColumnIndexOrThrow(Events.CALENDAR_ID);
239
240         String rRule = mCursor.getString(mEventIndexRrule);
241         boolean allDay = mCursor.getInt(indexAllDay) != 0;
242         long dtstart = mCursor.getLong(indexDtstart);
243         long id = mCursor.getInt(mEventIndexId);
244
245         // If the repeating event has not been given a sync id from the server
246         // yet, then we can't delete a single instance of this event.  (This is
247         // a deficiency in the CalendarProvider and sync code.) We checked for
248         // that when creating the list of items in the dialog and we removed
249         // the first element ("DELETE_SELECTED") from the dialog in that case.
250         // The "which" value is a 0-based index into the list of items, where
251         // the "DELETE_SELECTED" item is at index 0.
252         if (mSyncId == null) {
253             which += 1;
254         }
255         
256         switch (which) {
257             case DELETE_SELECTED:
258             {
259                 // If we are deleting the first event in the series, then
260                 // instead of creating a recurrence exception, just change
261                 // the start time of the recurrence.
262                 if (dtstart == mStartMillis) {
263                     // TODO
264                 }
265                 
266                 // Create a recurrence exception by creating a new event
267                 // with the status "cancelled".
268                 ContentValues values = new ContentValues();
269                 
270                 // The title might not be necessary, but it makes it easier
271                 // to find this entry in the database when there is a problem.
272                 String title = mCursor.getString(indexTitle);
273                 values.put(Events.TITLE, title);
274                 
275                 String timezone = mCursor.getString(indexTimezone);
276                 int calendarId = mCursor.getInt(indexCalendarId);
277                 values.put(Events.EVENT_TIMEZONE, timezone);
278                 values.put(Events.ALL_DAY, allDay ? 1 : 0);
279                 values.put(Events.CALENDAR_ID, calendarId);
280                 values.put(Events.DTSTART, mStartMillis);
281                 values.put(Events.DTEND, mEndMillis);
282                 values.put(Events.ORIGINAL_EVENT, mSyncId);
283                 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
284                 values.put(Events.STATUS, Events.STATUS_CANCELED);
285                 
286                 mContentResolver.insert(Events.CONTENT_URI, values);
287                 break;
288             }
289             case DELETE_ALL: {
290                 Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
291                 mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
292                 break;
293             }
294             case DELETE_ALL_FOLLOWING: {
295                 // If we are deleting the first event in the series and all
296                 // following events, then delete them all.
297                 if (dtstart == mStartMillis) {
298                     Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
299                     mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
300                     break;
301                 }
302                 
303                 // Modify the repeating event to end just before this event time
304                 EventRecurrence eventRecurrence = new EventRecurrence();
305                 eventRecurrence.parse(rRule);
306                 Time date = new Time();
307                 if (allDay) {
308                     date.timezone = Time.TIMEZONE_UTC;
309                 }
310                 date.set(mStartMillis);
311                 date.second--;
312                 date.normalize(false);
313                 
314                 // Google calendar seems to require the UNTIL string to be
315                 // in UTC.
316                 date.switchTimezone(Time.TIMEZONE_UTC);
317                 eventRecurrence.until = date.format2445();
318                 
319                 ContentValues values = new ContentValues();
320                 values.put(Events.DTSTART, dtstart);
321                 values.put(Events.RRULE, eventRecurrence.toString());
322                 Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
323                 mContentResolver.update(uri, values, null, null);
324                 break;
325             }
326         }
327         if (mExitWhenDone) {
328             mParent.finish();
329         }
330     }
331 }