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