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