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.calendarcommon.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                     .setIconAttribute(android.R.attr.alertDialogIcon)
    325                     .setSingleChoiceItems(adapter, which, mDeleteListListener)
    326                     .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
    327                     .setNegativeButton(android.R.string.cancel, null).show();
    328             dialog.setOnDismissListener(mDismissListener);
    329             mAlertDialog = dialog;
    330 
    331             if (which == -1) {
    332                 // Disable the "Ok" button until the user selects which events
    333                 // to delete.
    334                 Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
    335                 ok.setEnabled(false);
    336             }
    337         }
    338     }
    339 
    340     private void deleteExceptionEvent() {
    341         long id = mModel.mId; // mCursor.getInt(mEventIndexId);
    342 
    343         // update a recurrence exception by setting its status to "canceled"
    344         ContentValues values = new ContentValues();
    345         values.put(Events.STATUS, Events.STATUS_CANCELED);
    346 
    347         Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    348         mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
    349                 Utils.UNDO_DELAY);
    350     }
    351 
    352     private void deleteRepeatingEvent(int which) {
    353         String rRule = mModel.mRrule;
    354         boolean allDay = mModel.mAllDay;
    355         long dtstart = mModel.mStart;
    356         long id = mModel.mId; // mCursor.getInt(mEventIndexId);
    357 
    358         switch (which) {
    359             case DELETE_SELECTED: {
    360                 // If we are deleting the first event in the series, then
    361                 // instead of creating a recurrence exception, just change
    362                 // the start time of the recurrence.
    363                 if (dtstart == mStartMillis) {
    364                     // TODO
    365                 }
    366 
    367                 // Create a recurrence exception by creating a new event
    368                 // with the status "cancelled".
    369                 ContentValues values = new ContentValues();
    370 
    371                 // The title might not be necessary, but it makes it easier
    372                 // to find this entry in the database when there is a problem.
    373                 String title = mModel.mTitle;
    374                 values.put(Events.TITLE, title);
    375 
    376                 String timezone = mModel.mTimezone;
    377                 long calendarId = mModel.mCalendarId;
    378                 values.put(Events.EVENT_TIMEZONE, timezone);
    379                 values.put(Events.ALL_DAY, allDay ? 1 : 0);
    380                 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
    381                 values.put(Events.CALENDAR_ID, calendarId);
    382                 values.put(Events.DTSTART, mStartMillis);
    383                 values.put(Events.DTEND, mEndMillis);
    384                 values.put(Events.ORIGINAL_SYNC_ID, mSyncId);
    385                 values.put(Events.ORIGINAL_ID, id);
    386                 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
    387                 values.put(Events.STATUS, Events.STATUS_CANCELED);
    388 
    389                 mService.startInsert(mService.getNextToken(), null, Events.CONTENT_URI, values,
    390                         Utils.UNDO_DELAY);
    391                 break;
    392             }
    393             case DELETE_ALL: {
    394                 Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    395                 mService.startDelete(mService.getNextToken(), null, uri, null, null,
    396                         Utils.UNDO_DELAY);
    397                 break;
    398             }
    399             case DELETE_ALL_FOLLOWING: {
    400                 // If we are deleting the first event in the series and all
    401                 // following events, then delete them all.
    402                 if (dtstart == mStartMillis) {
    403                     Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    404                     mService.startDelete(mService.getNextToken(), null, uri, null, null,
    405                             Utils.UNDO_DELAY);
    406                     break;
    407                 }
    408 
    409                 // Modify the repeating event to end just before this event time
    410                 EventRecurrence eventRecurrence = new EventRecurrence();
    411                 eventRecurrence.parse(rRule);
    412                 Time date = new Time();
    413                 if (allDay) {
    414                     date.timezone = Time.TIMEZONE_UTC;
    415                 }
    416                 date.set(mStartMillis);
    417                 date.second--;
    418                 date.normalize(false);
    419 
    420                 // Google calendar seems to require the UNTIL string to be
    421                 // in UTC.
    422                 date.switchTimezone(Time.TIMEZONE_UTC);
    423                 eventRecurrence.until = date.format2445();
    424 
    425                 ContentValues values = new ContentValues();
    426                 values.put(Events.DTSTART, dtstart);
    427                 values.put(Events.RRULE, eventRecurrence.toString());
    428                 Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
    429                 mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
    430                         Utils.UNDO_DELAY);
    431                 break;
    432             }
    433         }
    434         if (mCallback != null) {
    435             mCallback.run();
    436         }
    437         if (mExitWhenDone) {
    438             mParent.finish();
    439         }
    440     }
    441 
    442     public void setDeleteNotificationListener(DeleteNotifyListener listener) {
    443         mDeleteStartedListener = listener;
    444     }
    445 
    446     private void deleteStarted() {
    447         if (mDeleteStartedListener != null) {
    448             mDeleteStartedListener.onDeleteStarted();
    449         }
    450     }
    451 
    452     public void setOnDismissListener(Dialog.OnDismissListener listener) {
    453         if (mAlertDialog != null) {
    454             mAlertDialog.setOnDismissListener(listener);
    455         }
    456         mDismissListener = listener;
    457     }
    458 
    459     public void dismissAlertDialog() {
    460         if (mAlertDialog != null) {
    461             mAlertDialog.dismiss();
    462         }
    463     }
    464 }
    465