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