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