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