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