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