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