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