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