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