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.format.Time; 31 import 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 */ 54 public 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 }; 88 89 private int mEventIndexId; 90 private int mEventIndexRrule; 91 private String mSyncId; 92 93 public DeleteEventHelper(Activity parent, boolean exitWhenDone) { 94 mParent = parent; 95 mContentResolver = mParent.getContentResolver(); 96 mExitWhenDone = exitWhenDone; 97 } 98 99 public void setExitWhenDone(boolean exitWhenDone) { 100 mExitWhenDone = exitWhenDone; 101 } 102 103 /** 104 * This callback is used when a normal event is deleted. 105 */ 106 private DialogInterface.OnClickListener mDeleteNormalDialogListener = 107 new DialogInterface.OnClickListener() { 108 public void onClick(DialogInterface dialog, int button) { 109 long id = mCursor.getInt(mEventIndexId); 110 Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id); 111 mContentResolver.delete(uri, null /* where */, null /* selectionArgs */); 112 if (mExitWhenDone) { 113 mParent.finish(); 114 } 115 } 116 }; 117 118 /** 119 * This callback is used when a list item for a repeating event is selected 120 */ 121 private DialogInterface.OnClickListener mDeleteListListener = 122 new DialogInterface.OnClickListener() { 123 public void onClick(DialogInterface dialog, int button) { 124 mWhichDelete = button; 125 126 // Enable the "ok" button now that the user has selected which 127 // events in the series to delete. 128 Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE); 129 ok.setEnabled(true); 130 } 131 }; 132 133 /** 134 * This callback is used when a repeating event is deleted. 135 */ 136 private DialogInterface.OnClickListener mDeleteRepeatingDialogListener = 137 new DialogInterface.OnClickListener() { 138 public void onClick(DialogInterface dialog, int button) { 139 if (mWhichDelete != -1) { 140 deleteRepeatingEvent(mWhichDelete); 141 } 142 } 143 }; 144 145 /** 146 * Does the required processing for deleting an event, which includes 147 * first popping up a dialog asking for confirmation (if the event is 148 * a normal event) or a dialog asking which events to delete (if the 149 * event is a repeating event). The "which" parameter is used to check 150 * the initial selection and is only used for repeating events. Set 151 * "which" to -1 to have nothing selected initially. 152 * 153 * @param begin the begin time of the event, in UTC milliseconds 154 * @param end the end time of the event, in UTC milliseconds 155 * @param eventId the event id 156 * @param which one of the values {@link DELETE_SELECTED}, 157 * {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1 158 */ 159 public void delete(long begin, long end, long eventId, int which) { 160 Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId); 161 Cursor cursor = mParent.managedQuery(uri, EVENT_PROJECTION, null, null, null); 162 if (cursor == null) { 163 return; 164 } 165 cursor.moveToFirst(); 166 delete(begin, end, cursor, which); 167 } 168 169 /** 170 * Does the required processing for deleting an event. This method 171 * takes a {@link Cursor} object as a parameter, which must point to 172 * a row in the Events table containing the required database fields. 173 * The required fields for a normal event are: 174 * 175 * <ul> 176 * <li> Events._ID </li> 177 * <li> Events.TITLE </li> 178 * <li> Events.RRULE </li> 179 * </ul> 180 * 181 * The required fields for a repeating event include the above plus the 182 * following fields: 183 * 184 * <ul> 185 * <li> Events.ALL_DAY </li> 186 * <li> Events.CALENDAR_ID </li> 187 * <li> Events.DTSTART </li> 188 * <li> Events._SYNC_ID </li> 189 * <li> Events.EVENT_TIMEZONE </li> 190 * </ul> 191 * 192 * @param begin the begin time of the event, in UTC milliseconds 193 * @param end the end time of the event, in UTC milliseconds 194 * @param cursor the database cursor containing the required fields 195 * @param which one of the values {@link DELETE_SELECTED}, 196 * {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1 197 */ 198 public void delete(long begin, long end, Cursor cursor, int which) { 199 mWhichDelete = which; 200 mStartMillis = begin; 201 mEndMillis = end; 202 mCursor = cursor; 203 mEventIndexId = mCursor.getColumnIndexOrThrow(Events._ID); 204 mEventIndexRrule = mCursor.getColumnIndexOrThrow(Events.RRULE); 205 int eventIndexSyncId = mCursor.getColumnIndexOrThrow(Events._SYNC_ID); 206 mSyncId = mCursor.getString(eventIndexSyncId); 207 208 // If this is a repeating event, then pop up a dialog asking the 209 // user if they want to delete all of the repeating events or 210 // just some of them. 211 String rRule = mCursor.getString(mEventIndexRrule); 212 if (rRule == null) { 213 // This is a normal event. Pop up a confirmation dialog. 214 new AlertDialog.Builder(mParent) 215 .setTitle(R.string.delete_title) 216 .setMessage(R.string.delete_this_event_title) 217 .setIcon(android.R.drawable.ic_dialog_alert) 218 .setPositiveButton(android.R.string.ok, mDeleteNormalDialogListener) 219 .setNegativeButton(android.R.string.cancel, null) 220 .show(); 221 } else { 222 // This is a repeating event. Pop up a dialog asking which events 223 // to delete. 224 int labelsArrayId = R.array.delete_repeating_labels; 225 if (mSyncId == null) { 226 labelsArrayId = R.array.delete_repeating_labels_no_selected; 227 } 228 AlertDialog dialog = new AlertDialog.Builder(mParent) 229 .setTitle(R.string.delete_title) 230 .setIcon(android.R.drawable.ic_dialog_alert) 231 .setSingleChoiceItems(labelsArrayId, which, mDeleteListListener) 232 .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener) 233 .setNegativeButton(android.R.string.cancel, null) 234 .show(); 235 mAlertDialog = dialog; 236 237 if (which == -1) { 238 // Disable the "Ok" button until the user selects which events 239 // to delete. 240 Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE); 241 ok.setEnabled(false); 242 } 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