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