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.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