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 
17 package com.android.calendar;
18 
19 import com.android.calendar.event.EditEventHelper;
20 import com.android.calendarcommon2.EventRecurrence;
21 
22 import android.app.Activity;
23 import android.app.AlertDialog;
24 import android.app.Dialog;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.provider.CalendarContract;
33 import android.provider.CalendarContract.Events;
34 import android.text.TextUtils;
35 import android.text.format.Time;
36 import android.widget.ArrayAdapter;
37 import android.widget.Button;
38 
39 import java.util.ArrayList;
40 import 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  */
63 public 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 {
onDeleteStarted()98         public void onDeleteStarted();
99     }
100 
101 
DeleteEventHelper(Context context, Activity parentActivity, boolean exitWhenDone)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 
setExitWhenDone(boolean exitWhenDone)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      */
delete(long begin, long end, long eventId, int which)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 
delete(long begin, long end, long eventId, int which, Runnable callback)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      */
delete(long begin, long end, CalendarEventModel model, int which)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 
deleteExceptionEvent()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 
deleteRepeatingEvent(int which)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 
setDeleteNotificationListener(DeleteNotifyListener listener)444     public void setDeleteNotificationListener(DeleteNotifyListener listener) {
445         mDeleteStartedListener = listener;
446     }
447 
deleteStarted()448     private void deleteStarted() {
449         if (mDeleteStartedListener != null) {
450             mDeleteStartedListener.onDeleteStarted();
451         }
452     }
453 
setOnDismissListener(Dialog.OnDismissListener listener)454     public void setOnDismissListener(Dialog.OnDismissListener listener) {
455         if (mAlertDialog != null) {
456             mAlertDialog.setOnDismissListener(listener);
457         }
458         mDismissListener = listener;
459     }
460 
dismissAlertDialog()461     public void dismissAlertDialog() {
462         if (mAlertDialog != null) {
463             mAlertDialog.dismiss();
464         }
465     }
466 }
467