DeleteEventHelper.java revision 1251cb05edd5283545bc27652f5c0aaa3d1095a1
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.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
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 Context mContext;
57
58    private long mStartMillis;
59    private long mEndMillis;
60    private CalendarEventModel mModel;
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 String mSyncId;
79
80    private AsyncQueryService mService;
81
82    public DeleteEventHelper(Context context, Activity parentActivity, boolean exitWhenDone) {
83        if (exitWhenDone && parentActivity == null) {
84            throw new IllegalArgumentException("parentActivity is required to exit when done");
85        }
86
87        mContext = context;
88        mParent = parentActivity;
89        // TODO move the creation of this service out into the activity.
90        mService = new AsyncQueryService(mContext) {
91            @Override
92            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
93                if (cursor == null) {
94                    return;
95                }
96                cursor.moveToFirst();
97                CalendarEventModel mModel = new CalendarEventModel();
98                EditEventHelper.setModelFromCursor(mModel, cursor);
99                cursor.close();
100                DeleteEventHelper.this.delete(mStartMillis, mEndMillis, mModel, mWhichDelete);
101            }
102        };
103        mExitWhenDone = exitWhenDone;
104    }
105
106    public void setAsyncQueryService(AsyncQueryService service) {
107        mService = service;
108    }
109
110    public void setExitWhenDone(boolean exitWhenDone) {
111        mExitWhenDone = exitWhenDone;
112    }
113
114    /**
115     * This callback is used when a normal event is deleted.
116     */
117    private DialogInterface.OnClickListener mDeleteNormalDialogListener =
118            new DialogInterface.OnClickListener() {
119        public void onClick(DialogInterface dialog, int button) {
120            long id = mModel.mId; // mCursor.getInt(mEventIndexId);
121            Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
122            mService.startDelete(mService.getNextToken(), null, uri, null, null, Utils.UNDO_DELAY);
123            if (mExitWhenDone) {
124                mParent.finish();
125            }
126        }
127    };
128
129    /**
130     * This callback is used when an exception to an event is deleted
131     */
132    private DialogInterface.OnClickListener mDeleteExceptionDialogListener =
133        new DialogInterface.OnClickListener() {
134        public void onClick(DialogInterface dialog, int button) {
135            deleteExceptionEvent();
136            if (mExitWhenDone) {
137                mParent.finish();
138            }
139        }
140    };
141
142    /**
143     * This callback is used when a list item for a repeating event is selected
144     */
145    private DialogInterface.OnClickListener mDeleteListListener =
146            new DialogInterface.OnClickListener() {
147        public void onClick(DialogInterface dialog, int button) {
148            mWhichDelete = button;
149
150            // Enable the "ok" button now that the user has selected which
151            // events in the series to delete.
152            Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
153            ok.setEnabled(true);
154        }
155    };
156
157    /**
158     * This callback is used when a repeating event is deleted.
159     */
160    private DialogInterface.OnClickListener mDeleteRepeatingDialogListener =
161            new DialogInterface.OnClickListener() {
162        public void onClick(DialogInterface dialog, int button) {
163            if (mWhichDelete != -1) {
164                deleteRepeatingEvent(mWhichDelete);
165            }
166        }
167    };
168
169    /**
170     * Does the required processing for deleting an event, which includes
171     * first popping up a dialog asking for confirmation (if the event is
172     * a normal event) or a dialog asking which events to delete (if the
173     * event is a repeating event).  The "which" parameter is used to check
174     * the initial selection and is only used for repeating events.  Set
175     * "which" to -1 to have nothing selected initially.
176     *
177     * @param begin the begin time of the event, in UTC milliseconds
178     * @param end the end time of the event, in UTC milliseconds
179     * @param eventId the event id
180     * @param which one of the values {@link DELETE_SELECTED},
181     *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
182     */
183    public void delete(long begin, long end, long eventId, int which) {
184        Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId);
185        mService.startQuery(mService.getNextToken(), null, uri, EditEventHelper.EVENT_PROJECTION,
186                null, null, null);
187        mStartMillis = begin;
188        mEndMillis = end;
189        mWhichDelete = which;
190    }
191
192    /**
193     * Does the required processing for deleting an event.  This method
194     * takes a {@link CalendarEventModel} object, which must have a valid
195     * uri for referencing the event in the database and have the required
196     * fields listed below.
197     * The required fields for a normal event are:
198     *
199     * <ul>
200     *   <li> Events._ID </li>
201     *   <li> Events.TITLE </li>
202     *   <li> Events.RRULE </li>
203     * </ul>
204     *
205     * The required fields for a repeating event include the above plus the
206     * following fields:
207     *
208     * <ul>
209     *   <li> Events.ALL_DAY </li>
210     *   <li> Events.CALENDAR_ID </li>
211     *   <li> Events.DTSTART </li>
212     *   <li> Events._SYNC_ID </li>
213     *   <li> Events.EVENT_TIMEZONE </li>
214     * </ul>
215     *
216     * If the event no longer exists in the db this will still prompt
217     * the user but will return without modifying the db after the query
218     * returns.
219     *
220     * @param begin the begin time of the event, in UTC milliseconds
221     * @param end the end time of the event, in UTC milliseconds
222     * @param cursor the database cursor containing the required fields
223     * @param which one of the values {@link DELETE_SELECTED},
224     *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
225     */
226    public void delete(long begin, long end, CalendarEventModel model, int which) {
227        mWhichDelete = which;
228        mStartMillis = begin;
229        mEndMillis = end;
230        mModel = model;
231        mSyncId = model.mSyncId;
232
233        // If this is a repeating event, then pop up a dialog asking the
234        // user if they want to delete all of the repeating events or
235        // just some of them.
236        String rRule = model.mRrule;
237        String originalEvent = model.mOriginalEvent;
238        if (rRule == null) {
239            AlertDialog dialog = new AlertDialog.Builder(mContext)
240            .setTitle(R.string.delete_title)
241            .setMessage(R.string.delete_this_event_title)
242            .setIcon(android.R.drawable.ic_dialog_alert)
243            .setNegativeButton(android.R.string.cancel, null)
244            .create();
245
246            if (originalEvent == null) {
247                // This is a normal event. Pop up a confirmation dialog.
248                dialog.setButton(DialogInterface.BUTTON_POSITIVE,
249                        mContext.getText(android.R.string.ok),
250                        mDeleteNormalDialogListener);
251            } else {
252                // This is an exception event. Pop up a confirmation dialog.
253                dialog.setButton(DialogInterface.BUTTON_POSITIVE,
254                        mContext.getText(android.R.string.ok),
255                        mDeleteExceptionDialogListener);
256            }
257            dialog.show();
258        } else {
259            // This is a repeating event.  Pop up a dialog asking which events
260            // to delete.
261            int labelsArrayId = R.array.delete_repeating_labels;
262            if (mSyncId == null) {
263                labelsArrayId = R.array.delete_repeating_labels_no_selected;
264                if (which > 0) {
265                    which--;
266                    mWhichDelete--;
267                }
268            }
269            AlertDialog dialog = new AlertDialog.Builder(mContext)
270            .setTitle(R.string.delete_title)
271            .setIcon(android.R.drawable.ic_dialog_alert)
272            .setSingleChoiceItems(labelsArrayId, which, mDeleteListListener)
273            .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
274            .setNegativeButton(android.R.string.cancel, null)
275            .show();
276            mAlertDialog = dialog;
277
278            if (which == -1) {
279                // Disable the "Ok" button until the user selects which events
280                // to delete.
281                Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
282                ok.setEnabled(false);
283            }
284        }
285    }
286
287    private void deleteExceptionEvent() {
288        long id = mModel.mId; // mCursor.getInt(mEventIndexId);
289
290        // update a recurrence exception by setting its status to "canceled"
291        ContentValues values = new ContentValues();
292        values.put(Events.STATUS, Events.STATUS_CANCELED);
293
294        Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
295        mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
296                Utils.UNDO_DELAY);
297    }
298
299    private void deleteRepeatingEvent(int which) {
300        String rRule = mModel.mRrule;
301        boolean allDay = mModel.mAllDay;
302        long dtstart = mModel.mStart;
303        long id = mModel.mId; // mCursor.getInt(mEventIndexId);
304
305        // If the repeating event has not been given a sync id from the server
306        // yet, then we can't delete a single instance of this event.  (This is
307        // a deficiency in the CalendarProvider and sync code.) We checked for
308        // that when creating the list of items in the dialog and we removed
309        // the first element ("DELETE_SELECTED") from the dialog in that case.
310        // The "which" value is a 0-based index into the list of items, where
311        // the "DELETE_SELECTED" item is at index 0.
312        if (mSyncId == null) {
313            which += 1;
314        }
315
316        switch (which) {
317            case DELETE_SELECTED: {
318                // If we are deleting the first event in the series, then
319                // instead of creating a recurrence exception, just change
320                // the start time of the recurrence.
321                if (dtstart == mStartMillis) {
322                    // TODO
323                }
324
325                // Create a recurrence exception by creating a new event
326                // with the status "cancelled".
327                ContentValues values = new ContentValues();
328
329                // The title might not be necessary, but it makes it easier
330                // to find this entry in the database when there is a problem.
331                String title = mModel.mTitle;
332                values.put(Events.TITLE, title);
333
334                String timezone = mModel.mTimezone;
335                long calendarId = mModel.mCalendarId;
336                values.put(Events.EVENT_TIMEZONE, timezone);
337                values.put(Events.ALL_DAY, allDay ? 1 : 0);
338                values.put(Events.CALENDAR_ID, calendarId);
339                values.put(Events.DTSTART, mStartMillis);
340                values.put(Events.DTEND, mEndMillis);
341                values.put(Events.ORIGINAL_EVENT, mSyncId);
342                values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
343                values.put(Events.STATUS, Events.STATUS_CANCELED);
344
345                mService.startInsert(mService.getNextToken(), null, Events.CONTENT_URI, values,
346                        Utils.UNDO_DELAY);
347                break;
348            }
349            case DELETE_ALL: {
350                Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
351                mService.startDelete(mService.getNextToken(), null, uri, null, null,
352                        Utils.UNDO_DELAY);
353                break;
354            }
355            case DELETE_ALL_FOLLOWING: {
356                // If we are deleting the first event in the series and all
357                // following events, then delete them all.
358                if (dtstart == mStartMillis) {
359                    Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
360                    mService.startDelete(mService.getNextToken(), null, uri, null, null,
361                            Utils.UNDO_DELAY);
362                    break;
363                }
364
365                // Modify the repeating event to end just before this event time
366                EventRecurrence eventRecurrence = new EventRecurrence();
367                eventRecurrence.parse(rRule);
368                Time date = new Time();
369                if (allDay) {
370                    date.timezone = Time.TIMEZONE_UTC;
371                }
372                date.set(mStartMillis);
373                date.second--;
374                date.normalize(false);
375
376                // Google calendar seems to require the UNTIL string to be
377                // in UTC.
378                date.switchTimezone(Time.TIMEZONE_UTC);
379                eventRecurrence.until = date.format2445();
380
381                ContentValues values = new ContentValues();
382                values.put(Events.DTSTART, dtstart);
383                values.put(Events.RRULE, eventRecurrence.toString());
384                Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
385                mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
386                        Utils.UNDO_DELAY);
387                break;
388            }
389        }
390        if (mExitWhenDone) {
391            mParent.finish();
392        }
393    }
394}
395