DeleteEventHelper.java revision 146de36083f6ce8b7e8a1f974d3990594a36bfec
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
56    private static final String TAG = "DeleteEventHelper";
57    private final Activity mParent;
58    private final ContentResolver mContentResolver;
59
60    private long mStartMillis;
61    private long mEndMillis;
62    private Cursor mCursor;
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    static final int DELETE_SELECTED = 0;
74    static final int DELETE_ALL_FOLLOWING = 1;
75    static final int DELETE_ALL = 2;
76
77    private int mWhichDelete;
78    private AlertDialog mAlertDialog;
79
80    private static final String[] EVENT_PROJECTION = new String[] {
81        Events._ID,
82        Events.TITLE,
83        Events.ALL_DAY,
84        Events.CALENDAR_ID,
85        Events.RRULE,
86        Events.DTSTART,
87        Events._SYNC_ID,
88        Events.EVENT_TIMEZONE,
89    };
90
91    private int mEventIndexId;
92    private int mEventIndexRrule;
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 a list item for a repeating event is selected
122     */
123    private DialogInterface.OnClickListener mDeleteListListener =
124            new DialogInterface.OnClickListener() {
125        public void onClick(DialogInterface dialog, int button) {
126            mWhichDelete = button;
127
128            // Enable the "ok" button now that the user has selected which
129            // events in the series to delete.
130            Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
131            ok.setEnabled(true);
132        }
133    };
134
135    /**
136     * This callback is used when a repeating event is deleted.
137     */
138    private DialogInterface.OnClickListener mDeleteRepeatingDialogListener =
139            new DialogInterface.OnClickListener() {
140        public void onClick(DialogInterface dialog, int button) {
141            if (mWhichDelete != -1) {
142                deleteRepeatingEvent(mWhichDelete);
143            }
144        }
145    };
146
147    /**
148     * Does the required processing for deleting an event, which includes
149     * first popping up a dialog asking for confirmation (if the event is
150     * a normal event) or a dialog asking which events to delete (if the
151     * event is a repeating event).  The "which" parameter is used to check
152     * the initial selection and is only used for repeating events.  Set
153     * "which" to -1 to have nothing selected initially.
154     *
155     * @param begin the begin time of the event, in UTC milliseconds
156     * @param end the end time of the event, in UTC milliseconds
157     * @param eventId the event id
158     * @param which one of the values {@link DELETE_SELECTED},
159     *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
160     */
161    public void delete(long begin, long end, long eventId, int which) {
162        Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId);
163        Cursor cursor = mParent.managedQuery(uri, EVENT_PROJECTION, null, null);
164        if (cursor == null) {
165            return;
166        }
167        cursor.moveToFirst();
168        delete(begin, end, cursor, which);
169    }
170
171    /**
172     * Does the required processing for deleting an event.  This method
173     * takes a {@link Cursor} object as a parameter, which must point to
174     * a row in the Events table containing the required database fields.
175     * The required fields for a normal event are:
176     *
177     * <ul>
178     *   <li> Events._ID </li>
179     *   <li> Events.TITLE </li>
180     *   <li> Events.RRULE </li>
181     * </ul>
182     *
183     * The required fields for a repeating event include the above plus the
184     * following fields:
185     *
186     * <ul>
187     *   <li> Events.ALL_DAY </li>
188     *   <li> Events.CALENDAR_ID </li>
189     *   <li> Events.DTSTART </li>
190     *   <li> Events._SYNC_ID </li>
191     *   <li> Events.EVENT_TIMEZONE </li>
192     * </ul>
193     *
194     * @param begin the begin time of the event, in UTC milliseconds
195     * @param end the end time of the event, in UTC milliseconds
196     * @param cursor the database cursor containing the required fields
197     * @param which one of the values {@link DELETE_SELECTED},
198     *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
199     */
200    public void delete(long begin, long end, Cursor cursor, int which) {
201        mWhichDelete = which;
202        mStartMillis = begin;
203        mEndMillis = end;
204        mCursor = cursor;
205        mEventIndexId = mCursor.getColumnIndexOrThrow(Events._ID);
206        mEventIndexRrule = mCursor.getColumnIndexOrThrow(Events.RRULE);
207        int eventIndexSyncId = mCursor.getColumnIndexOrThrow(Events._SYNC_ID);
208        mSyncId = mCursor.getString(eventIndexSyncId);
209
210        // If this is a repeating event, then pop up a dialog asking the
211        // user if they want to delete all of the repeating events or
212        // just some of them.
213        String rRule = mCursor.getString(mEventIndexRrule);
214        if (rRule == null) {
215            // This is a normal event. Pop up a confirmation dialog.
216            new AlertDialog.Builder(mParent)
217            .setTitle(R.string.delete_title)
218            .setMessage(R.string.delete_this_event_title)
219            .setIcon(android.R.drawable.ic_dialog_alert)
220            .setPositiveButton(android.R.string.ok, mDeleteNormalDialogListener)
221            .setNegativeButton(android.R.string.cancel, null)
222            .show();
223        } else {
224            // This is a repeating event.  Pop up a dialog asking which events
225            // to delete.
226            int labelsArrayId = R.array.delete_repeating_labels;
227            if (mSyncId == null) {
228                labelsArrayId = R.array.delete_repeating_labels_no_selected;
229            }
230            AlertDialog dialog = new AlertDialog.Builder(mParent)
231            .setTitle(R.string.delete_title)
232            .setIcon(android.R.drawable.ic_dialog_alert)
233            .setSingleChoiceItems(labelsArrayId, which, mDeleteListListener)
234            .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
235            .setNegativeButton(android.R.string.cancel, null)
236            .show();
237            mAlertDialog = dialog;
238
239            // Disable the "Ok" button until the user selects which events to
240            // delete.
241            Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
242            ok.setEnabled(false);
243        }
244    }
245
246    private void deleteRepeatingEvent(int which) {
247        int indexDtstart = mCursor.getColumnIndexOrThrow(Events.DTSTART);
248        int indexAllDay = mCursor.getColumnIndexOrThrow(Events.ALL_DAY);
249        int indexTitle = mCursor.getColumnIndexOrThrow(Events.TITLE);
250        int indexTimezone = mCursor.getColumnIndexOrThrow(Events.EVENT_TIMEZONE);
251        int indexCalendarId = mCursor.getColumnIndexOrThrow(Events.CALENDAR_ID);
252
253        String rRule = mCursor.getString(mEventIndexRrule);
254        boolean allDay = mCursor.getInt(indexAllDay) != 0;
255        long dtstart = mCursor.getLong(indexDtstart);
256        long id = mCursor.getInt(mEventIndexId);
257
258        // If the repeating event has not been given a sync id from the server
259        // yet, then we can't delete a single instance of this event.  (This is
260        // a deficiency in the CalendarProvider and sync code.) We checked for
261        // that when creating the list of items in the dialog and we removed
262        // the first element ("DELETE_SELECTED") from the dialog in that case.
263        // The "which" value is a 0-based index into the list of items, where
264        // the "DELETE_SELECTED" item is at index 0.
265        if (mSyncId == null) {
266            which += 1;
267        }
268
269        switch (which) {
270            case DELETE_SELECTED:
271            {
272                // If we are deleting the first event in the series, then
273                // instead of creating a recurrence exception, just change
274                // the start time of the recurrence.
275                if (dtstart == mStartMillis) {
276                    // TODO
277                }
278
279                // Create a recurrence exception by creating a new event
280                // with the status "cancelled".
281                ContentValues values = new ContentValues();
282
283                // The title might not be necessary, but it makes it easier
284                // to find this entry in the database when there is a problem.
285                String title = mCursor.getString(indexTitle);
286                values.put(Events.TITLE, title);
287
288                String timezone = mCursor.getString(indexTimezone);
289                int calendarId = mCursor.getInt(indexCalendarId);
290                values.put(Events.EVENT_TIMEZONE, timezone);
291                values.put(Events.ALL_DAY, allDay ? 1 : 0);
292                values.put(Events.CALENDAR_ID, calendarId);
293                values.put(Events.DTSTART, mStartMillis);
294                values.put(Events.DTEND, mEndMillis);
295                values.put(Events.ORIGINAL_EVENT, mSyncId);
296                values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
297                values.put(Events.STATUS, Events.STATUS_CANCELED);
298
299                mContentResolver.insert(Events.CONTENT_URI, values);
300                break;
301            }
302            case DELETE_ALL: {
303                Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
304                mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
305                break;
306            }
307            case DELETE_ALL_FOLLOWING: {
308                // If we are deleting the first event in the series and all
309                // following events, then delete them all.
310                if (dtstart == mStartMillis) {
311                    Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
312                    mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
313                    break;
314                }
315
316                // Modify the repeating event to end just before this event time
317                EventRecurrence eventRecurrence = new EventRecurrence();
318                eventRecurrence.parse(rRule);
319                Time date = new Time();
320                if (allDay) {
321                    date.timezone = Time.TIMEZONE_UTC;
322                }
323                date.set(mStartMillis);
324                date.second--;
325                date.normalize(false);
326
327                // Google calendar seems to require the UNTIL string to be
328                // in UTC.
329                date.switchTimezone(Time.TIMEZONE_UTC);
330                eventRecurrence.until = date.format2445();
331
332                ContentValues values = new ContentValues();
333                values.put(Events.DTSTART, dtstart);
334                values.put(Events.RRULE, eventRecurrence.toString());
335                Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
336                mContentResolver.update(uri, values, null, null);
337                break;
338            }
339        }
340        if (mExitWhenDone) {
341            mParent.finish();
342        }
343    }
344}
345