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 com.android.calendar.event.EditEventHelper;
     20 import com.android.calendarcommon2.EventRecurrence;
     21 
     22 import android.app.Activity;
     23 import android.app.AlertDialog;
     24 import android.app.Dialog;
     25 import android.content.ContentUris;
     26 import android.content.ContentValues;
     27 import android.content.Context;
     28 import android.content.DialogInterface;
     29 import android.content.res.Resources;
     30 import android.database.Cursor;
     31 import android.net.Uri;
     32 import android.provider.CalendarContract;
     33 import android.provider.CalendarContract.Events;
     34 import android.text.TextUtils;
     35 import android.text.format.Time;
     36 import android.widget.ArrayAdapter;
     37 import android.widget.Button;
     38 
     39 import java.util.ArrayList;
     40 import java.util.Arrays;
     41 
     42 /**
     43  * A helper class for deleting events.  If a normal event is selected for
     44  * deletion, then this pops up a confirmation dialog.  If the user confirms,
     45  * then the normal event is deleted.
     46  *
     47  * <p>
     48  * If a repeating event is selected for deletion, then this pops up dialog
     49  * asking if the user wants to delete just this one instance, or all the
     50  * events in the series, or this event plus all following events.  The user
     51  * may also cancel the delete.
     52  * </p>
     53  *
     54  * <p>
     55  * To use this class, create an instance, passing in the parent activity
     56  * and a boolean that determines if the parent activity should exit if the
     57  * event is deleted.  Then to use the instance, call one of the
     58  * {@link delete()} methods on this class.
     59  *
     60  * An instance of this class may be created once and reused (by calling
     61  * {@link #delete()} multiple times).
     62  */
     63 public class DeleteEventHelper {
     64     private final Activity mParent;
     65     private Context mContext;
     66 
     67     private long mStartMillis;
     68     private long mEndMillis;
     69     private CalendarEventModel mModel;
     70 
     71     /**
     72      * If true, then call finish() on the parent activity when done.
     73      */
     74     private boolean mExitWhenDone;
     75     // the runnable to execute when the delete is confirmed
     76     private Runnable mCallback;
     77 
     78     /**
     79      * These are the corresponding indices into the array of strings
     80      * "R.array.delete_repeating_labels" in the resource file.
     81      */
     82     public static final int DELETE_SELECTED = 0;
     83     public static final int DELETE_ALL_FOLLOWING = 1;
     84     public static final int DELETE_ALL = 2;
     85 
     86     private int mWhichDelete;
     87     private ArrayList<Integer> mWhichIndex;
     88     private AlertDialog mAlertDialog;
     89     private Dialog.OnDismissListener mDismissListener;
     90 
     91     private String mSyncId;
     92 
     93     private AsyncQueryService mService;
     94 
     95     private DeleteNotifyListener mDeleteStartedListener = null;
     96 
     97     public interface DeleteNotifyListener {
     98         public void onDeleteStarted();
     99     }
    100 
    101 
    102     public DeleteEventHelper(Context context, Activity parentActivity, boolean exitWhenDone) {
    103         if (exitWhenDone && parentActivity == null) {
    104             throw new IllegalArgumentException("parentActivity is required to exit when done");
    105         }
    106 
    107         mContext = context;
    108         mParent = parentActivity;
    109         // TODO move the creation of this service out into the activity.
    110         mService = new AsyncQueryService(mContext) {
    111             @Override
    112             protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    113                 if (cursor == null) {
    114                     return;
    115                 }
    116                 cursor.moveToFirst();
    117                 CalendarEventModel mModel = new CalendarEventModel();
    118                 EditEventHelper.setModelFromCursor(mModel, cursor);
    119                 cursor.close();
    120                 DeleteEventHelper.this.delete(mStartMillis, mEndMillis, mModel, mWhichDelete);
    121             }
    122         };
    123         mExitWhenDone = exitWhenDone;
    124     }
    125 
    126     public void setExitWhenDone(boolean exitWhenDone) {
    127         mExitWhenDone = exitWhenDone;
    128     }
    129 
    130     /**
    131      * This callback is used when a normal event is deleted.
    132      */
    133     private DialogInterface.OnClickListener mDeleteNormalDialogListener =
    134             new DialogInterface.OnClickListener() {
    135         public void onClick(DialogInterface dialog, int button) {
    136             deleteStarted();
    137             long id = mModel.mId; // mCursor.getInt(mEventIndexId);
    138             Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    139             mService.startDelete(mService.getNextToken(), null, uri, null, null, Utils.UNDO_DELAY);
    140             if (mCallback != null) {
    141                 mCallback.run();
    142             }
    143             if (mExitWhenDone) {
    144                 mParent.finish();
    145             }
    146         }
    147     };
    148 
    149     /**
    150      * This callback is used when an exception to an event is deleted
    151      */
    152     private DialogInterface.OnClickListener mDeleteExceptionDialogListener =
    153         new DialogInterface.OnClickListener() {
    154         public void onClick(DialogInterface dialog, int button) {
    155             deleteStarted();
    156             deleteExceptionEvent();
    157             if (mCallback != null) {
    158                 mCallback.run();
    159             }
    160             if (mExitWhenDone) {
    161                 mParent.finish();
    162             }
    163         }
    164     };
    165 
    166     /**
    167      * This callback is used when a list item for a repeating event is selected
    168      */
    169     private DialogInterface.OnClickListener mDeleteListListener =
    170             new DialogInterface.OnClickListener() {
    171         public void onClick(DialogInterface dialog, int button) {
    172             // set mWhichDelete to the delete type at that index
    173             mWhichDelete = mWhichIndex.get(button);
    174 
    175             // Enable the "ok" button now that the user has selected which
    176             // events in the series to delete.
    177             Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
    178             ok.setEnabled(true);
    179         }
    180     };
    181 
    182     /**
    183      * This callback is used when a repeating event is deleted.
    184      */
    185     private DialogInterface.OnClickListener mDeleteRepeatingDialogListener =
    186             new DialogInterface.OnClickListener() {
    187         public void onClick(DialogInterface dialog, int button) {
    188             deleteStarted();
    189             if (mWhichDelete != -1) {
    190                 deleteRepeatingEvent(mWhichDelete);
    191             }
    192         }
    193     };
    194 
    195     /**
    196      * Does the required processing for deleting an event, which includes
    197      * first popping up a dialog asking for confirmation (if the event is
    198      * a normal event) or a dialog asking which events to delete (if the
    199      * event is a repeating event).  The "which" parameter is used to check
    200      * the initial selection and is only used for repeating events.  Set
    201      * "which" to -1 to have nothing selected initially.
    202      *
    203      * @param begin the begin time of the event, in UTC milliseconds
    204      * @param end the end time of the event, in UTC milliseconds
    205      * @param eventId the event id
    206      * @param which one of the values {@link DELETE_SELECTED},
    207      *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
    208      */
    209     public void delete(long begin, long end, long eventId, int which) {
    210         Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);
    211         mService.startQuery(mService.getNextToken(), null, uri, EditEventHelper.EVENT_PROJECTION,
    212                 null, null, null);
    213         mStartMillis = begin;
    214         mEndMillis = end;
    215         mWhichDelete = which;
    216     }
    217 
    218     public void delete(long begin, long end, long eventId, int which, Runnable callback) {
    219         delete(begin, end, eventId, which);
    220         mCallback = callback;
    221     }
    222 
    223     /**
    224      * Does the required processing for deleting an event.  This method
    225      * takes a {@link CalendarEventModel} object, which must have a valid
    226      * uri for referencing the event in the database and have the required
    227      * fields listed below.
    228      * The required fields for a normal event are:
    229      *
    230      * <ul>
    231      *   <li> Events._ID </li>
    232      *   <li> Events.TITLE </li>
    233      *   <li> Events.RRULE </li>
    234      * </ul>
    235      *
    236      * The required fields for a repeating event include the above plus the
    237      * following fields:
    238      *
    239      * <ul>
    240      *   <li> Events.ALL_DAY </li>
    241      *   <li> Events.CALENDAR_ID </li>
    242      *   <li> Events.DTSTART </li>
    243      *   <li> Events._SYNC_ID </li>
    244      *   <li> Events.EVENT_TIMEZONE </li>
    245      * </ul>
    246      *
    247      * If the event no longer exists in the db this will still prompt
    248      * the user but will return without modifying the db after the query
    249      * returns.
    250      *
    251      * @param begin the begin time of the event, in UTC milliseconds
    252      * @param end the end time of the event, in UTC milliseconds
    253      * @param cursor the database cursor containing the required fields
    254      * @param which one of the values {@link DELETE_SELECTED},
    255      *  {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
    256      */
    257     public void delete(long begin, long end, CalendarEventModel model, int which) {
    258         mWhichDelete = which;
    259         mStartMillis = begin;
    260         mEndMillis = end;
    261         mModel = model;
    262         mSyncId = model.mSyncId;
    263 
    264         // If this is a repeating event, then pop up a dialog asking the
    265         // user if they want to delete all of the repeating events or
    266         // just some of them.
    267         String rRule = model.mRrule;
    268         String originalEvent = model.mOriginalSyncId;
    269         if (TextUtils.isEmpty(rRule)) {
    270             AlertDialog dialog = new AlertDialog.Builder(mContext)
    271                     .setMessage(R.string.delete_this_event_title)
    272                     .setIconAttribute(android.R.attr.alertDialogIcon)
    273                     .setNegativeButton(android.R.string.cancel, null).create();
    274 
    275             if (originalEvent == null) {
    276                 // This is a normal event. Pop up a confirmation dialog.
    277                 dialog.setButton(DialogInterface.BUTTON_POSITIVE,
    278                         mContext.getText(android.R.string.ok),
    279                         mDeleteNormalDialogListener);
    280             } else {
    281                 // This is an exception event. Pop up a confirmation dialog.
    282                 dialog.setButton(DialogInterface.BUTTON_POSITIVE,
    283                         mContext.getText(android.R.string.ok),
    284                         mDeleteExceptionDialogListener);
    285             }
    286             dialog.setOnDismissListener(mDismissListener);
    287             dialog.show();
    288             mAlertDialog = dialog;
    289         } else {
    290             // This is a repeating event.  Pop up a dialog asking which events
    291             // to delete.
    292             Resources res = mContext.getResources();
    293             ArrayList<String> labelArray = new ArrayList<String>(Arrays.asList(res
    294                     .getStringArray(R.array.delete_repeating_labels)));
    295             // asList doesn't like int[] so creating it manually.
    296             int[] labelValues = res.getIntArray(R.array.delete_repeating_values);
    297             ArrayList<Integer> labelIndex = new ArrayList<Integer>();
    298             for (int val : labelValues) {
    299                 labelIndex.add(val);
    300             }
    301 
    302             if (mSyncId == null) {
    303                 // remove 'Only this event' item
    304                 labelArray.remove(0);
    305                 labelIndex.remove(0);
    306                 if (!model.mIsOrganizer) {
    307                     // remove 'This and future events' item
    308                     labelArray.remove(0);
    309                     labelIndex.remove(0);
    310                 }
    311             } else if (!model.mIsOrganizer) {
    312                 // remove 'This and future events' item
    313                 labelArray.remove(1);
    314                 labelIndex.remove(1);
    315             }
    316             if (which != -1) {
    317                 // transform the which to the index in the array
    318                 which = labelIndex.indexOf(which);
    319             }
    320             mWhichIndex = labelIndex;
    321             ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext,
    322                     android.R.layout.simple_list_item_single_choice, labelArray);
    323             AlertDialog dialog = new AlertDialog.Builder(mContext)
    324                     .setTitle(
    325                             mContext.getString(R.string.delete_recurring_event_title,model.mTitle))
    326                     .setIconAttribute(android.R.attr.alertDialogIcon)
    327                     .setSingleChoiceItems(adapter, which, mDeleteListListener)
    328                     .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
    329                     .setNegativeButton(android.R.string.cancel, null).show();
    330             dialog.setOnDismissListener(mDismissListener);
    331             mAlertDialog = dialog;
    332 
    333             if (which == -1) {
    334                 // Disable the "Ok" button until the user selects which events
    335                 // to delete.
    336                 Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
    337                 ok.setEnabled(false);
    338             }
    339         }
    340     }
    341 
    342     private void deleteExceptionEvent() {
    343         long id = mModel.mId; // mCursor.getInt(mEventIndexId);
    344 
    345         // update a recurrence exception by setting its status to "canceled"
    346         ContentValues values = new ContentValues();
    347         values.put(Events.STATUS, Events.STATUS_CANCELED);
    348 
    349         Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    350         mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
    351                 Utils.UNDO_DELAY);
    352     }
    353 
    354     private void deleteRepeatingEvent(int which) {
    355         String rRule = mModel.mRrule;
    356         boolean allDay = mModel.mAllDay;
    357         long dtstart = mModel.mStart;
    358         long id = mModel.mId; // mCursor.getInt(mEventIndexId);
    359 
    360         switch (which) {
    361             case DELETE_SELECTED: {
    362                 // If we are deleting the first event in the series, then
    363                 // instead of creating a recurrence exception, just change
    364                 // the start time of the recurrence.
    365                 if (dtstart == mStartMillis) {
    366                     // TODO
    367                 }
    368 
    369                 // Create a recurrence exception by creating a new event
    370                 // with the status "cancelled".
    371                 ContentValues values = new ContentValues();
    372 
    373                 // The title might not be necessary, but it makes it easier
    374                 // to find this entry in the database when there is a problem.
    375                 String title = mModel.mTitle;
    376                 values.put(Events.TITLE, title);
    377 
    378                 String timezone = mModel.mTimezone;
    379                 long calendarId = mModel.mCalendarId;
    380                 values.put(Events.EVENT_TIMEZONE, timezone);
    381                 values.put(Events.ALL_DAY, allDay ? 1 : 0);
    382                 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
    383                 values.put(Events.CALENDAR_ID, calendarId);
    384                 values.put(Events.DTSTART, mStartMillis);
    385                 values.put(Events.DTEND, mEndMillis);
    386                 values.put(Events.ORIGINAL_SYNC_ID, mSyncId);
    387                 values.put(Events.ORIGINAL_ID, id);
    388                 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
    389                 values.put(Events.STATUS, Events.STATUS_CANCELED);
    390 
    391                 mService.startInsert(mService.getNextToken(), null, Events.CONTENT_URI, values,
    392                         Utils.UNDO_DELAY);
    393                 break;
    394             }
    395             case DELETE_ALL: {
    396                 Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    397                 mService.startDelete(mService.getNextToken(), null, uri, null, null,
    398                         Utils.UNDO_DELAY);
    399                 break;
    400             }
    401             case DELETE_ALL_FOLLOWING: {
    402                 // If we are deleting the first event in the series and all
    403                 // following events, then delete them all.
    404                 if (dtstart == mStartMillis) {
    405                     Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    406                     mService.startDelete(mService.getNextToken(), null, uri, null, null,
    407                             Utils.UNDO_DELAY);
    408                     break;
    409                 }
    410 
    411                 // Modify the repeating event to end just before this event time
    412                 EventRecurrence eventRecurrence = new EventRecurrence();
    413                 eventRecurrence.parse(rRule);
    414                 Time date = new Time();
    415                 if (allDay) {
    416                     date.timezone = Time.TIMEZONE_UTC;
    417                 }
    418                 date.set(mStartMillis);
    419                 date.second--;
    420                 date.normalize(false);
    421 
    422                 // Google calendar seems to require the UNTIL string to be
    423                 // in UTC.
    424                 date.switchTimezone(Time.TIMEZONE_UTC);
    425                 eventRecurrence.until = date.format2445();
    426 
    427                 ContentValues values = new ContentValues();
    428                 values.put(Events.DTSTART, dtstart);
    429                 values.put(Events.RRULE, eventRecurrence.toString());
    430                 Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    431                 mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
    432                         Utils.UNDO_DELAY);
    433                 break;
    434             }
    435         }
    436         if (mCallback != null) {
    437             mCallback.run();
    438         }
    439         if (mExitWhenDone) {
    440             mParent.finish();
    441         }
    442     }
    443 
    444     public void setDeleteNotificationListener(DeleteNotifyListener listener) {
    445         mDeleteStartedListener = listener;
    446     }
    447 
    448     private void deleteStarted() {
    449         if (mDeleteStartedListener != null) {
    450             mDeleteStartedListener.onDeleteStarted();
    451         }
    452     }
    453 
    454     public void setOnDismissListener(Dialog.OnDismissListener listener) {
    455         if (mAlertDialog != null) {
    456             mAlertDialog.setOnDismissListener(listener);
    457         }
    458         mDismissListener = listener;
    459     }
    460 
    461     public void dismissAlertDialog() {
    462         if (mAlertDialog != null) {
    463             mAlertDialog.dismiss();
    464         }
    465     }
    466 }
    467