DeleteEventHelper.java revision 83f7ce18540aa5b8ed2402d178d35520cd725dea
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
17package com.android.calendar;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.DialogInterface;
25import android.database.Cursor;
26import android.net.Uri;
27import android.pim.EventRecurrence;
28import android.provider.Calendar;
29import android.provider.Calendar.Events;
30import android.text.format.Time;
31import android.widget.Button;
32
33/**
34 * A helper class for deleting events.  If a normal event is selected for
35 * deletion, then this pops up a confirmation dialog.  If the user confirms,
36 * then the normal event is deleted.
37 *
38 * <p>
39 * If a repeating event is selected for deletion, then this pops up dialog
40 * asking if the user wants to delete just this one instance, or all the
41 * events in the series, or this event plus all following events.  The user
42 * may also cancel the delete.
43 * </p>
44 *
45 * <p>
46 * To use this class, create an instance, passing in the parent activity
47 * and a boolean that determines if the parent activity should exit if the
48 * event is deleted.  Then to use the instance, call one of the
49 * {@link delete()} methods on this class.
50 *
51 * An instance of this class may be created once and reused (by calling
52 * {@link #delete()} multiple times).
53 */
54public class DeleteEventHelper {
55    private final Activity mParent;
56    private final ContentResolver mContentResolver;
57
58    private long mStartMillis;
59    private long mEndMillis;
60    private Cursor mCursor;
61
62    /**
63     * If true, then call finish() on the parent activity when done.
64     */
65    private boolean mExitWhenDone;
66
67    /**
68     * These are the corresponding indices into the array of strings
69     * "R.array.delete_repeating_labels" in the resource file.
70     */
71    static final int DELETE_SELECTED = 0;
72    static final int DELETE_ALL_FOLLOWING = 1;
73    static final int DELETE_ALL = 2;
74
75    private int mWhichDelete;
76    private AlertDialog mAlertDialog;
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        Events.ORIGINAL_EVENT,
88    };
89
90    private int mEventIndexId;
91    private int mEventIndexRrule;
92    private int mEventIndexOriginalEvent;
93    private String mSyncId;
94
95    public DeleteEventHelper(Activity parent, boolean exitWhenDone) {
96        mParent = parent;
97        mContentResolver = mParent.getContentResolver();
98        mExitWhenDone = exitWhenDone;
99    }
100
101    public void setExitWhenDone(boolean exitWhenDone) {
102        mExitWhenDone = exitWhenDone;
103    }
104
105    /**
106     * This callback is used when a normal event is deleted.
107     */
108    private DialogInterface.OnClickListener mDeleteNormalDialogListener =
109            new DialogInterface.OnClickListener() {
110        public void onClick(DialogInterface dialog, int button) {
111            long id = mCursor.getInt(mEventIndexId);
112            Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
113            mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
114            if (mExitWhenDone) {
115                mParent.finish();
116            }
117        }
118    };
119
120    /**
121     * This callback is used when an exception to an event is deleted
122     */
123    private DialogInterface.OnClickListener mDeleteExceptionDialogListener =
124        new DialogInterface.OnClickListener() {
125        public void onClick(DialogInterface dialog, int button) {
126            deleteExceptionEvent();
127            if (mExitWhenDone) {
128                mParent.finish();
129            }
130        }
131    };
132
133    /**
134     * This callback is used when a list item for a repeating event is selected
135     */
136    private DialogInterface.OnClickListener mDeleteListListener =
137            new DialogInterface.OnClickListener() {
138        public void onClick(DialogInterface dialog, int button) {
139            mWhichDelete = button;
140
141            // Enable the "ok" button now that the user has selected which
142            // events in the series to delete.
143            Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
144            ok.setEnabled(true);
145        }
146    };
147
148    /**
149     * This callback is used when a repeating event is deleted.
150     */
151    private DialogInterface.OnClickListener mDeleteRepeatingDialogListener =
152            new DialogInterface.OnClickListener() {
153        public void onClick(DialogInterface dialog, int button) {
154            if (mWhichDelete != -1) {
155                deleteRepeatingEvent(mWhichDelete);
156            }
157        }
158    };
159
160    /**
161     * Does the required processing for deleting an event, which includes
162     * first popping up a dialog asking for confirmation (if the event is
163     * a normal event) or a dialog asking which events to delete (if the
164     * event is a repeating event).  The "which" parameter is used to check
165     * the initial selection and is only used for repeating events.  Set
166     * "which" to -1 to have nothing selected initially.
167     *
168     * @param begin the begin time of the event, in UTC milliseconds
169     * @param end the end time of the event, in UTC milliseconds
170     * @param eventId the event id
171     * @param which one of the values {@link DELETE_SELECTED},
172     *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
173     */
174    public void delete(long begin, long end, long eventId, int which) {
175        Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId);
176        Cursor cursor = mParent.managedQuery(uri, EVENT_PROJECTION, null, null, null);
177        if (cursor == null) {
178            return;
179        }
180        cursor.moveToFirst();
181        delete(begin, end, cursor, which);
182    }
183
184    /**
185     * Does the required processing for deleting an event.  This method
186     * takes a {@link Cursor} object as a parameter, which must point to
187     * a row in the Events table containing the required database fields.
188     * The required fields for a normal event are:
189     *
190     * <ul>
191     *   <li> Events._ID </li>
192     *   <li> Events.TITLE </li>
193     *   <li> Events.RRULE </li>
194     * </ul>
195     *
196     * The required fields for a repeating event include the above plus the
197     * following fields:
198     *
199     * <ul>
200     *   <li> Events.ALL_DAY </li>
201     *   <li> Events.CALENDAR_ID </li>
202     *   <li> Events.DTSTART </li>
203     *   <li> Events._SYNC_ID </li>
204     *   <li> Events.EVENT_TIMEZONE </li>
205     * </ul>
206     *
207     * @param begin the begin time of the event, in UTC milliseconds
208     * @param end the end time of the event, in UTC milliseconds
209     * @param cursor the database cursor containing the required fields
210     * @param which one of the values {@link DELETE_SELECTED},
211     *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
212     */
213    public void delete(long begin, long end, Cursor cursor, int which) {
214        mWhichDelete = which;
215        mStartMillis = begin;
216        mEndMillis = end;
217        mCursor = cursor;
218        mEventIndexId = mCursor.getColumnIndexOrThrow(Events._ID);
219        mEventIndexRrule = mCursor.getColumnIndexOrThrow(Events.RRULE);
220        mEventIndexOriginalEvent = mCursor.getColumnIndexOrThrow(Events.ORIGINAL_EVENT);
221        int eventIndexSyncId = mCursor.getColumnIndexOrThrow(Events._SYNC_ID);
222        mSyncId = mCursor.getString(eventIndexSyncId);
223
224        // If this is a repeating event, then pop up a dialog asking the
225        // user if they want to delete all of the repeating events or
226        // just some of them.
227        String rRule = mCursor.getString(mEventIndexRrule);
228        String originalEvent = mCursor.getString(this.mEventIndexOriginalEvent);
229        if (rRule == null) {
230            AlertDialog dialog = new AlertDialog.Builder(mParent)
231            .setTitle(R.string.delete_title)
232            .setMessage(R.string.delete_this_event_title)
233            .setIcon(android.R.drawable.ic_dialog_alert)
234            .setNegativeButton(android.R.string.cancel, null)
235            .create();
236
237            if (originalEvent == null) {
238                // This is a normal event. Pop up a confirmation dialog.
239                dialog.setButton(DialogInterface.BUTTON_POSITIVE,
240                        mParent.getText(android.R.string.ok),
241                        mDeleteNormalDialogListener);
242            } else {
243                // This is an exception event. Pop up a confirmation dialog.
244                dialog.setButton(DialogInterface.BUTTON_POSITIVE,
245                        mParent.getText(android.R.string.ok),
246                        mDeleteExceptionDialogListener);
247            }
248            dialog.show();
249        } else {
250            // This is a repeating event.  Pop up a dialog asking which events
251            // to delete.
252            int labelsArrayId = R.array.delete_repeating_labels;
253            if (mSyncId == null) {
254                labelsArrayId = R.array.delete_repeating_labels_no_selected;
255            }
256            AlertDialog dialog = new AlertDialog.Builder(mParent)
257            .setTitle(R.string.delete_title)
258            .setIcon(android.R.drawable.ic_dialog_alert)
259            .setSingleChoiceItems(labelsArrayId, which, mDeleteListListener)
260            .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
261            .setNegativeButton(android.R.string.cancel, null)
262            .show();
263            mAlertDialog = dialog;
264
265            if (which == -1) {
266                // Disable the "Ok" button until the user selects which events
267                // to delete.
268                Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
269                ok.setEnabled(false);
270            }
271        }
272    }
273
274    private void deleteExceptionEvent() {
275        long id = mCursor.getInt(mEventIndexId);
276
277        // update a recurrence exception by setting its status to "canceled"
278        ContentValues values = new ContentValues();
279        values.put(Events.STATUS, Events.STATUS_CANCELED);
280
281        Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
282        mContentResolver.update(uri, values, null, null);
283    }
284
285    private void deleteRepeatingEvent(int which) {
286        int indexDtstart = mCursor.getColumnIndexOrThrow(Events.DTSTART);
287        int indexAllDay = mCursor.getColumnIndexOrThrow(Events.ALL_DAY);
288        int indexTitle = mCursor.getColumnIndexOrThrow(Events.TITLE);
289        int indexTimezone = mCursor.getColumnIndexOrThrow(Events.EVENT_TIMEZONE);
290        int indexCalendarId = mCursor.getColumnIndexOrThrow(Events.CALENDAR_ID);
291
292        String rRule = mCursor.getString(mEventIndexRrule);
293        boolean allDay = mCursor.getInt(indexAllDay) != 0;
294        long dtstart = mCursor.getLong(indexDtstart);
295        long id = mCursor.getInt(mEventIndexId);
296
297        // If the repeating event has not been given a sync id from the server
298        // yet, then we can't delete a single instance of this event.  (This is
299        // a deficiency in the CalendarProvider and sync code.) We checked for
300        // that when creating the list of items in the dialog and we removed
301        // the first element ("DELETE_SELECTED") from the dialog in that case.
302        // The "which" value is a 0-based index into the list of items, where
303        // the "DELETE_SELECTED" item is at index 0.
304        if (mSyncId == null) {
305            which += 1;
306        }
307
308        switch (which) {
309            case DELETE_SELECTED:
310            {
311                // If we are deleting the first event in the series, then
312                // instead of creating a recurrence exception, just change
313                // the start time of the recurrence.
314                if (dtstart == mStartMillis) {
315                    // TODO
316                }
317
318                // Create a recurrence exception by creating a new event
319                // with the status "cancelled".
320                ContentValues values = new ContentValues();
321
322                // The title might not be necessary, but it makes it easier
323                // to find this entry in the database when there is a problem.
324                String title = mCursor.getString(indexTitle);
325                values.put(Events.TITLE, title);
326
327                String timezone = mCursor.getString(indexTimezone);
328                int calendarId = mCursor.getInt(indexCalendarId);
329                values.put(Events.EVENT_TIMEZONE, timezone);
330                values.put(Events.ALL_DAY, allDay ? 1 : 0);
331                values.put(Events.CALENDAR_ID, calendarId);
332                values.put(Events.DTSTART, mStartMillis);
333                values.put(Events.DTEND, mEndMillis);
334                values.put(Events.ORIGINAL_EVENT, mSyncId);
335                values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
336                values.put(Events.STATUS, Events.STATUS_CANCELED);
337
338                mContentResolver.insert(Events.CONTENT_URI, values);
339                break;
340            }
341            case DELETE_ALL: {
342                Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
343                mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
344                break;
345            }
346            case DELETE_ALL_FOLLOWING: {
347                // If we are deleting the first event in the series and all
348                // following events, then delete them all.
349                if (dtstart == mStartMillis) {
350                    Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
351                    mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
352                    break;
353                }
354
355                // Modify the repeating event to end just before this event time
356                EventRecurrence eventRecurrence = new EventRecurrence();
357                eventRecurrence.parse(rRule);
358                Time date = new Time();
359                if (allDay) {
360                    date.timezone = Time.TIMEZONE_UTC;
361                }
362                date.set(mStartMillis);
363                date.second--;
364                date.normalize(false);
365
366                // Google calendar seems to require the UNTIL string to be
367                // in UTC.
368                date.switchTimezone(Time.TIMEZONE_UTC);
369                eventRecurrence.until = date.format2445();
370
371                ContentValues values = new ContentValues();
372                values.put(Events.DTSTART, dtstart);
373                values.put(Events.RRULE, eventRecurrence.toString());
374                Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
375                mContentResolver.update(uri, values, null, null);
376                break;
377            }
378        }
379        if (mExitWhenDone) {
380            mParent.finish();
381        }
382    }
383}
384