Home | History | Annotate | Download | only in calendar
      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