/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.calendar;
import com.android.calendar.event.EditEventHelper;
import com.android.calendarcommon2.EventRecurrence;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Events;
import android.text.TextUtils;
import android.text.format.Time;
import android.widget.ArrayAdapter;
import android.widget.Button;
import java.util.ArrayList;
import java.util.Arrays;
/**
* A helper class for deleting events. If a normal event is selected for
* deletion, then this pops up a confirmation dialog. If the user confirms,
* then the normal event is deleted.
*
*
* If a repeating event is selected for deletion, then this pops up dialog
* asking if the user wants to delete just this one instance, or all the
* events in the series, or this event plus all following events. The user
* may also cancel the delete.
*
*
*
* To use this class, create an instance, passing in the parent activity
* and a boolean that determines if the parent activity should exit if the
* event is deleted. Then to use the instance, call one of the
* {@link delete()} methods on this class.
*
* An instance of this class may be created once and reused (by calling
* {@link #delete()} multiple times).
*/
public class DeleteEventHelper {
private final Activity mParent;
private Context mContext;
private long mStartMillis;
private long mEndMillis;
private CalendarEventModel mModel;
/**
* If true, then call finish() on the parent activity when done.
*/
private boolean mExitWhenDone;
// the runnable to execute when the delete is confirmed
private Runnable mCallback;
/**
* These are the corresponding indices into the array of strings
* "R.array.delete_repeating_labels" in the resource file.
*/
public static final int DELETE_SELECTED = 0;
public static final int DELETE_ALL_FOLLOWING = 1;
public static final int DELETE_ALL = 2;
private int mWhichDelete;
private ArrayList mWhichIndex;
private AlertDialog mAlertDialog;
private Dialog.OnDismissListener mDismissListener;
private String mSyncId;
private AsyncQueryService mService;
private DeleteNotifyListener mDeleteStartedListener = null;
public interface DeleteNotifyListener {
public void onDeleteStarted();
}
public DeleteEventHelper(Context context, Activity parentActivity, boolean exitWhenDone) {
if (exitWhenDone && parentActivity == null) {
throw new IllegalArgumentException("parentActivity is required to exit when done");
}
mContext = context;
mParent = parentActivity;
// TODO move the creation of this service out into the activity.
mService = new AsyncQueryService(mContext) {
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (cursor == null) {
return;
}
cursor.moveToFirst();
CalendarEventModel mModel = new CalendarEventModel();
EditEventHelper.setModelFromCursor(mModel, cursor);
cursor.close();
DeleteEventHelper.this.delete(mStartMillis, mEndMillis, mModel, mWhichDelete);
}
};
mExitWhenDone = exitWhenDone;
}
public void setExitWhenDone(boolean exitWhenDone) {
mExitWhenDone = exitWhenDone;
}
/**
* This callback is used when a normal event is deleted.
*/
private DialogInterface.OnClickListener mDeleteNormalDialogListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int button) {
deleteStarted();
long id = mModel.mId; // mCursor.getInt(mEventIndexId);
Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
mService.startDelete(mService.getNextToken(), null, uri, null, null, Utils.UNDO_DELAY);
if (mCallback != null) {
mCallback.run();
}
if (mExitWhenDone) {
mParent.finish();
}
}
};
/**
* This callback is used when an exception to an event is deleted
*/
private DialogInterface.OnClickListener mDeleteExceptionDialogListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int button) {
deleteStarted();
deleteExceptionEvent();
if (mCallback != null) {
mCallback.run();
}
if (mExitWhenDone) {
mParent.finish();
}
}
};
/**
* This callback is used when a list item for a repeating event is selected
*/
private DialogInterface.OnClickListener mDeleteListListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int button) {
// set mWhichDelete to the delete type at that index
mWhichDelete = mWhichIndex.get(button);
// Enable the "ok" button now that the user has selected which
// events in the series to delete.
Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
ok.setEnabled(true);
}
};
/**
* This callback is used when a repeating event is deleted.
*/
private DialogInterface.OnClickListener mDeleteRepeatingDialogListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int button) {
deleteStarted();
if (mWhichDelete != -1) {
deleteRepeatingEvent(mWhichDelete);
}
}
};
/**
* Does the required processing for deleting an event, which includes
* first popping up a dialog asking for confirmation (if the event is
* a normal event) or a dialog asking which events to delete (if the
* event is a repeating event). The "which" parameter is used to check
* the initial selection and is only used for repeating events. Set
* "which" to -1 to have nothing selected initially.
*
* @param begin the begin time of the event, in UTC milliseconds
* @param end the end time of the event, in UTC milliseconds
* @param eventId the event id
* @param which one of the values {@link DELETE_SELECTED},
* {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
*/
public void delete(long begin, long end, long eventId, int which) {
Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);
mService.startQuery(mService.getNextToken(), null, uri, EditEventHelper.EVENT_PROJECTION,
null, null, null);
mStartMillis = begin;
mEndMillis = end;
mWhichDelete = which;
}
public void delete(long begin, long end, long eventId, int which, Runnable callback) {
delete(begin, end, eventId, which);
mCallback = callback;
}
/**
* Does the required processing for deleting an event. This method
* takes a {@link CalendarEventModel} object, which must have a valid
* uri for referencing the event in the database and have the required
* fields listed below.
* The required fields for a normal event are:
*
*
* - Events._ID
* - Events.TITLE
* - Events.RRULE
*
*
* The required fields for a repeating event include the above plus the
* following fields:
*
*
* - Events.ALL_DAY
* - Events.CALENDAR_ID
* - Events.DTSTART
* - Events._SYNC_ID
* - Events.EVENT_TIMEZONE
*
*
* If the event no longer exists in the db this will still prompt
* the user but will return without modifying the db after the query
* returns.
*
* @param begin the begin time of the event, in UTC milliseconds
* @param end the end time of the event, in UTC milliseconds
* @param cursor the database cursor containing the required fields
* @param which one of the values {@link DELETE_SELECTED},
* {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
*/
public void delete(long begin, long end, CalendarEventModel model, int which) {
mWhichDelete = which;
mStartMillis = begin;
mEndMillis = end;
mModel = model;
mSyncId = model.mSyncId;
// If this is a repeating event, then pop up a dialog asking the
// user if they want to delete all of the repeating events or
// just some of them.
String rRule = model.mRrule;
String originalEvent = model.mOriginalSyncId;
if (TextUtils.isEmpty(rRule)) {
AlertDialog dialog = new AlertDialog.Builder(mContext)
.setMessage(R.string.delete_this_event_title)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setNegativeButton(android.R.string.cancel, null).create();
if (originalEvent == null) {
// This is a normal event. Pop up a confirmation dialog.
dialog.setButton(DialogInterface.BUTTON_POSITIVE,
mContext.getText(android.R.string.ok),
mDeleteNormalDialogListener);
} else {
// This is an exception event. Pop up a confirmation dialog.
dialog.setButton(DialogInterface.BUTTON_POSITIVE,
mContext.getText(android.R.string.ok),
mDeleteExceptionDialogListener);
}
dialog.setOnDismissListener(mDismissListener);
dialog.show();
mAlertDialog = dialog;
} else {
// This is a repeating event. Pop up a dialog asking which events
// to delete.
Resources res = mContext.getResources();
ArrayList labelArray = new ArrayList(Arrays.asList(res
.getStringArray(R.array.delete_repeating_labels)));
// asList doesn't like int[] so creating it manually.
int[] labelValues = res.getIntArray(R.array.delete_repeating_values);
ArrayList labelIndex = new ArrayList();
for (int val : labelValues) {
labelIndex.add(val);
}
if (mSyncId == null) {
// remove 'Only this event' item
labelArray.remove(0);
labelIndex.remove(0);
if (!model.mIsOrganizer) {
// remove 'This and future events' item
labelArray.remove(0);
labelIndex.remove(0);
}
} else if (!model.mIsOrganizer) {
// remove 'This and future events' item
labelArray.remove(1);
labelIndex.remove(1);
}
if (which != -1) {
// transform the which to the index in the array
which = labelIndex.indexOf(which);
}
mWhichIndex = labelIndex;
ArrayAdapter adapter = new ArrayAdapter(mContext,
android.R.layout.simple_list_item_single_choice, labelArray);
AlertDialog dialog = new AlertDialog.Builder(mContext)
.setTitle(
mContext.getString(R.string.delete_recurring_event_title,model.mTitle))
.setIconAttribute(android.R.attr.alertDialogIcon)
.setSingleChoiceItems(adapter, which, mDeleteListListener)
.setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
.setNegativeButton(android.R.string.cancel, null).show();
dialog.setOnDismissListener(mDismissListener);
mAlertDialog = dialog;
if (which == -1) {
// Disable the "Ok" button until the user selects which events
// to delete.
Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
ok.setEnabled(false);
}
}
}
private void deleteExceptionEvent() {
long id = mModel.mId; // mCursor.getInt(mEventIndexId);
// update a recurrence exception by setting its status to "canceled"
ContentValues values = new ContentValues();
values.put(Events.STATUS, Events.STATUS_CANCELED);
Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
Utils.UNDO_DELAY);
}
private void deleteRepeatingEvent(int which) {
String rRule = mModel.mRrule;
boolean allDay = mModel.mAllDay;
long dtstart = mModel.mStart;
long id = mModel.mId; // mCursor.getInt(mEventIndexId);
switch (which) {
case DELETE_SELECTED: {
// If we are deleting the first event in the series, then
// instead of creating a recurrence exception, just change
// the start time of the recurrence.
if (dtstart == mStartMillis) {
// TODO
}
// Create a recurrence exception by creating a new event
// with the status "cancelled".
ContentValues values = new ContentValues();
// The title might not be necessary, but it makes it easier
// to find this entry in the database when there is a problem.
String title = mModel.mTitle;
values.put(Events.TITLE, title);
String timezone = mModel.mTimezone;
long calendarId = mModel.mCalendarId;
values.put(Events.EVENT_TIMEZONE, timezone);
values.put(Events.ALL_DAY, allDay ? 1 : 0);
values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
values.put(Events.CALENDAR_ID, calendarId);
values.put(Events.DTSTART, mStartMillis);
values.put(Events.DTEND, mEndMillis);
values.put(Events.ORIGINAL_SYNC_ID, mSyncId);
values.put(Events.ORIGINAL_ID, id);
values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
values.put(Events.STATUS, Events.STATUS_CANCELED);
mService.startInsert(mService.getNextToken(), null, Events.CONTENT_URI, values,
Utils.UNDO_DELAY);
break;
}
case DELETE_ALL: {
Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
mService.startDelete(mService.getNextToken(), null, uri, null, null,
Utils.UNDO_DELAY);
break;
}
case DELETE_ALL_FOLLOWING: {
// If we are deleting the first event in the series and all
// following events, then delete them all.
if (dtstart == mStartMillis) {
Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
mService.startDelete(mService.getNextToken(), null, uri, null, null,
Utils.UNDO_DELAY);
break;
}
// Modify the repeating event to end just before this event time
EventRecurrence eventRecurrence = new EventRecurrence();
eventRecurrence.parse(rRule);
Time date = new Time();
if (allDay) {
date.timezone = Time.TIMEZONE_UTC;
}
date.set(mStartMillis);
date.second--;
date.normalize(false);
// Google calendar seems to require the UNTIL string to be
// in UTC.
date.switchTimezone(Time.TIMEZONE_UTC);
eventRecurrence.until = date.format2445();
ContentValues values = new ContentValues();
values.put(Events.DTSTART, dtstart);
values.put(Events.RRULE, eventRecurrence.toString());
Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
mService.startUpdate(mService.getNextToken(), null, uri, values, null, null,
Utils.UNDO_DELAY);
break;
}
}
if (mCallback != null) {
mCallback.run();
}
if (mExitWhenDone) {
mParent.finish();
}
}
public void setDeleteNotificationListener(DeleteNotifyListener listener) {
mDeleteStartedListener = listener;
}
private void deleteStarted() {
if (mDeleteStartedListener != null) {
mDeleteStartedListener.onDeleteStarted();
}
}
public void setOnDismissListener(Dialog.OnDismissListener listener) {
if (mAlertDialog != null) {
mAlertDialog.setOnDismissListener(listener);
}
mDismissListener = listener;
}
public void dismissAlertDialog() {
if (mAlertDialog != null) {
mAlertDialog.dismiss();
}
}
}