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