blob: c735e91e3b15cf8aafb5c48944ba9c407c0f13ca [file] [log] [blame]
/*
* Copyright (C) 2010 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.event;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Colors;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.util.Log;
import android.view.View;
import com.android.calendar.AbstractCalendarActivity;
import com.android.calendar.AsyncQueryService;
import com.android.calendar.CalendarEventModel;
import com.android.calendar.CalendarEventModel.Attendee;
import com.android.calendar.CalendarEventModel.ReminderEntry;
import com.android.calendar.Utils;
import com.android.calendarcommon2.DateException;
import com.android.calendarcommon2.EventRecurrence;
import com.android.calendarcommon2.RecurrenceProcessor;
import com.android.calendarcommon2.RecurrenceSet;
import com.android.common.Rfc822Validator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.TimeZone;
public class EditEventHelper {
private static final String TAG = "EditEventHelper";
private static final boolean DEBUG = false;
// Used for parsing rrules for special cases.
private EventRecurrence mEventRecurrence = new EventRecurrence();
private static final String NO_EVENT_COLOR = "";
public static final String[] EVENT_PROJECTION = new String[] {
Events._ID, // 0
Events.TITLE, // 1
Events.DESCRIPTION, // 2
Events.EVENT_LOCATION, // 3
Events.ALL_DAY, // 4
Events.HAS_ALARM, // 5
Events.CALENDAR_ID, // 6
Events.DTSTART, // 7
Events.DTEND, // 8
Events.DURATION, // 9
Events.EVENT_TIMEZONE, // 10
Events.RRULE, // 11
Events._SYNC_ID, // 12
Events.AVAILABILITY, // 13
Events.ACCESS_LEVEL, // 14
Events.OWNER_ACCOUNT, // 15
Events.HAS_ATTENDEE_DATA, // 16
Events.ORIGINAL_SYNC_ID, // 17
Events.ORGANIZER, // 18
Events.GUESTS_CAN_MODIFY, // 19
Events.ORIGINAL_ID, // 20
Events.STATUS, // 21
Events.CALENDAR_COLOR, // 22
Events.EVENT_COLOR, // 23
Events.EVENT_COLOR_KEY // 24
};
protected static final int EVENT_INDEX_ID = 0;
protected static final int EVENT_INDEX_TITLE = 1;
protected static final int EVENT_INDEX_DESCRIPTION = 2;
protected static final int EVENT_INDEX_EVENT_LOCATION = 3;
protected static final int EVENT_INDEX_ALL_DAY = 4;
protected static final int EVENT_INDEX_HAS_ALARM = 5;
protected static final int EVENT_INDEX_CALENDAR_ID = 6;
protected static final int EVENT_INDEX_DTSTART = 7;
protected static final int EVENT_INDEX_DTEND = 8;
protected static final int EVENT_INDEX_DURATION = 9;
protected static final int EVENT_INDEX_TIMEZONE = 10;
protected static final int EVENT_INDEX_RRULE = 11;
protected static final int EVENT_INDEX_SYNC_ID = 12;
protected static final int EVENT_INDEX_AVAILABILITY = 13;
protected static final int EVENT_INDEX_ACCESS_LEVEL = 14;
protected static final int EVENT_INDEX_OWNER_ACCOUNT = 15;
protected static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 16;
protected static final int EVENT_INDEX_ORIGINAL_SYNC_ID = 17;
protected static final int EVENT_INDEX_ORGANIZER = 18;
protected static final int EVENT_INDEX_GUESTS_CAN_MODIFY = 19;
protected static final int EVENT_INDEX_ORIGINAL_ID = 20;
protected static final int EVENT_INDEX_EVENT_STATUS = 21;
protected static final int EVENT_INDEX_CALENDAR_COLOR = 22;
protected static final int EVENT_INDEX_EVENT_COLOR = 23;
protected static final int EVENT_INDEX_EVENT_COLOR_KEY = 24;
public static final String[] REMINDERS_PROJECTION = new String[] {
Reminders._ID, // 0
Reminders.MINUTES, // 1
Reminders.METHOD, // 2
};
public static final int REMINDERS_INDEX_MINUTES = 1;
public static final int REMINDERS_INDEX_METHOD = 2;
public static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?";
// Visible for testing
static final String ATTENDEES_DELETE_PREFIX = Attendees.EVENT_ID + "=? AND "
+ Attendees.ATTENDEE_EMAIL + " IN (";
public static final int DOES_NOT_REPEAT = 0;
public static final int REPEATS_DAILY = 1;
public static final int REPEATS_EVERY_WEEKDAY = 2;
public static final int REPEATS_WEEKLY_ON_DAY = 3;
public static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
public static final int REPEATS_MONTHLY_ON_DAY = 5;
public static final int REPEATS_YEARLY = 6;
public static final int REPEATS_CUSTOM = 7;
protected static final int MODIFY_UNINITIALIZED = 0;
protected static final int MODIFY_SELECTED = 1;
protected static final int MODIFY_ALL_FOLLOWING = 2;
protected static final int MODIFY_ALL = 3;
protected static final int DAY_IN_SECONDS = 24 * 60 * 60;
private final AsyncQueryService mService;
// This allows us to flag the event if something is wrong with it, right now
// if an uri is provided for an event that doesn't exist in the db.
protected boolean mEventOk = true;
public static final int ATTENDEE_ID_NONE = -1;
public static final int[] ATTENDEE_VALUES = {
Attendees.ATTENDEE_STATUS_NONE,
Attendees.ATTENDEE_STATUS_ACCEPTED,
Attendees.ATTENDEE_STATUS_TENTATIVE,
Attendees.ATTENDEE_STATUS_DECLINED,
};
/**
* This is the symbolic name for the key used to pass in the boolean for
* creating all-day events that is part of the extra data of the intent.
* This is used only for creating new events and is set to true if the
* default for the new event should be an all-day event.
*/
public static final String EVENT_ALL_DAY = "allDay";
static final String[] CALENDARS_PROJECTION = new String[] {
Calendars._ID, // 0
Calendars.CALENDAR_DISPLAY_NAME, // 1
Calendars.OWNER_ACCOUNT, // 2
Calendars.CALENDAR_COLOR, // 3
Calendars.CAN_ORGANIZER_RESPOND, // 4
Calendars.CALENDAR_ACCESS_LEVEL, // 5
Calendars.VISIBLE, // 6
Calendars.MAX_REMINDERS, // 7
Calendars.ALLOWED_REMINDERS, // 8
Calendars.ALLOWED_ATTENDEE_TYPES, // 9
Calendars.ALLOWED_AVAILABILITY, // 10
Calendars.ACCOUNT_NAME, // 11
Calendars.ACCOUNT_TYPE, //12
};
static final int CALENDARS_INDEX_ID = 0;
static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
static final int CALENDARS_INDEX_COLOR = 3;
static final int CALENDARS_INDEX_CAN_ORGANIZER_RESPOND = 4;
static final int CALENDARS_INDEX_ACCESS_LEVEL = 5;
static final int CALENDARS_INDEX_VISIBLE = 6;
static final int CALENDARS_INDEX_MAX_REMINDERS = 7;
static final int CALENDARS_INDEX_ALLOWED_REMINDERS = 8;
static final int CALENDARS_INDEX_ALLOWED_ATTENDEE_TYPES = 9;
static final int CALENDARS_INDEX_ALLOWED_AVAILABILITY = 10;
static final int CALENDARS_INDEX_ACCOUNT_NAME = 11;
static final int CALENDARS_INDEX_ACCOUNT_TYPE = 12;
static final String CALENDARS_WHERE_WRITEABLE_VISIBLE = Calendars.CALENDAR_ACCESS_LEVEL + ">="
+ Calendars.CAL_ACCESS_CONTRIBUTOR + " AND " + Calendars.VISIBLE + "=1";
static final String CALENDARS_WHERE = Calendars._ID + "=?";
static final String[] COLORS_PROJECTION = new String[] {
Colors._ID, // 0
Colors.ACCOUNT_NAME,
Colors.ACCOUNT_TYPE,
Colors.COLOR, // 1
Colors.COLOR_KEY // 2
};
static final String COLORS_WHERE = Colors.ACCOUNT_NAME + "=? AND " + Colors.ACCOUNT_TYPE +
"=? AND " + Colors.COLOR_TYPE + "=" + Colors.TYPE_EVENT;
static final int COLORS_INDEX_ACCOUNT_NAME = 1;
static final int COLORS_INDEX_ACCOUNT_TYPE = 2;
static final int COLORS_INDEX_COLOR = 3;
static final int COLORS_INDEX_COLOR_KEY = 4;
static final String[] ATTENDEES_PROJECTION = new String[] {
Attendees._ID, // 0
Attendees.ATTENDEE_NAME, // 1
Attendees.ATTENDEE_EMAIL, // 2
Attendees.ATTENDEE_RELATIONSHIP, // 3
Attendees.ATTENDEE_STATUS, // 4
};
static final int ATTENDEES_INDEX_ID = 0;
static final int ATTENDEES_INDEX_NAME = 1;
static final int ATTENDEES_INDEX_EMAIL = 2;
static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
static final int ATTENDEES_INDEX_STATUS = 4;
static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=? AND attendeeEmail IS NOT NULL";
public static class AttendeeItem {
public boolean mRemoved;
public Attendee mAttendee;
public Drawable mBadge;
public int mUpdateCounts;
public View mView;
public Uri mContactLookupUri;
public AttendeeItem(Attendee attendee, Drawable badge) {
mAttendee = attendee;
mBadge = badge;
}
}
public EditEventHelper(Context context) {
mService = ((AbstractCalendarActivity)context).getAsyncQueryService();
}
public EditEventHelper(Context context, CalendarEventModel model) {
this(context);
// TODO: Remove unnecessary constructor.
}
/**
* Saves the event. Returns true if the event was successfully saved, false
* otherwise.
*
* @param model The event model to save
* @param originalModel A model of the original event if it exists
* @param modifyWhich For recurring events which type of series modification to use
* @return true if the event was successfully queued for saving
*/
public boolean saveEvent(CalendarEventModel model, CalendarEventModel originalModel,
int modifyWhich) {
boolean forceSaveReminders = false;
if (DEBUG) {
Log.d(TAG, "Saving event model: " + model);
}
if (!mEventOk) {
if (DEBUG) {
Log.w(TAG, "Event no longer exists. Event was not saved.");
}
return false;
}
// It's a problem if we try to save a non-existent or invalid model or if we're
// modifying an existing event and we have the wrong original model
if (model == null) {
Log.e(TAG, "Attempted to save null model.");
return false;
}
if (!model.isValid()) {
Log.e(TAG, "Attempted to save invalid model.");
return false;
}
if (originalModel != null && !isSameEvent(model, originalModel)) {
Log.e(TAG, "Attempted to update existing event but models didn't refer to the same "
+ "event.");
return false;
}
if (originalModel != null && model.isUnchanged(originalModel)) {
return false;
}
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
int eventIdIndex = -1;
ContentValues values = getContentValuesFromModel(model);
if (model.mUri != null && originalModel == null) {
Log.e(TAG, "Existing event but no originalModel provided. Aborting save.");
return false;
}
Uri uri = null;
if (model.mUri != null) {
uri = Uri.parse(model.mUri);
}
// Update the "hasAlarm" field for the event
ArrayList<ReminderEntry> reminders = model.mReminders;
int len = reminders.size();
values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
if (uri == null) {
// Add hasAttendeeData for a new event
values.put(Events.HAS_ATTENDEE_DATA, 1);
values.put(Events.STATUS, Events.STATUS_CONFIRMED);
eventIdIndex = ops.size();
ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(
Events.CONTENT_URI).withValues(values);
ops.add(b.build());
forceSaveReminders = true;
} else if (TextUtils.isEmpty(model.mRrule) && TextUtils.isEmpty(originalModel.mRrule)) {
// Simple update to a non-recurring event
checkTimeDependentFields(originalModel, model, values, modifyWhich);
ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
} else if (TextUtils.isEmpty(originalModel.mRrule)) {
// This event was changed from a non-repeating event to a
// repeating event.
ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
} else if (modifyWhich == MODIFY_SELECTED) {
// Modify contents of the current instance of repeating event
// Create a recurrence exception
long begin = model.mOriginalStart;
values.put(Events.ORIGINAL_SYNC_ID, originalModel.mSyncId);
values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
boolean allDay = originalModel.mAllDay;
values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
values.put(Events.STATUS, originalModel.mEventStatus);
eventIdIndex = ops.size();
ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(
Events.CONTENT_URI).withValues(values);
ops.add(b.build());
forceSaveReminders = true;
} else if (modifyWhich == MODIFY_ALL_FOLLOWING) {
if (TextUtils.isEmpty(model.mRrule)) {
// We've changed a recurring event to a non-recurring event.
// If the event we are editing is the first in the series,
// then delete the whole series. Otherwise, update the series
// to end at the new start time.
if (isFirstEventInSeries(model, originalModel)) {
ops.add(ContentProviderOperation.newDelete(uri).build());
} else {
// Update the current repeating event to end at the new start time. We
// ignore the RRULE returned because the exception event doesn't want one.
updatePastEvents(ops, originalModel, model.mOriginalStart);
}
eventIdIndex = ops.size();
values.put(Events.STATUS, originalModel.mEventStatus);
ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
.build());
} else {
if (isFirstEventInSeries(model, originalModel)) {
checkTimeDependentFields(originalModel, model, values, modifyWhich);
ContentProviderOperation.Builder b = ContentProviderOperation.newUpdate(uri)
.withValues(values);
ops.add(b.build());
} else {
// We need to update the existing recurrence to end before the exception
// event starts. If the recurrence rule has a COUNT, we need to adjust
// that in the original and in the exception. This call rewrites the
// original event's recurrence rule (in "ops"), and returns a new rule
// for the exception. If the exception explicitly set a new rule, however,
// we don't want to overwrite it.
String newRrule = updatePastEvents(ops, originalModel, model.mOriginalStart);
if (model.mRrule.equals(originalModel.mRrule)) {
values.put(Events.RRULE, newRrule);
}
// Create a new event with the user-modified fields
eventIdIndex = ops.size();
values.put(Events.STATUS, originalModel.mEventStatus);
ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
values).build());
}
}
forceSaveReminders = true;
} else if (modifyWhich == MODIFY_ALL) {
// Modify all instances of repeating event
if (TextUtils.isEmpty(model.mRrule)) {
// We've changed a recurring event to a non-recurring event.
// Delete the whole series and replace it with a new
// non-recurring event.
ops.add(ContentProviderOperation.newDelete(uri).build());
eventIdIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
.build());
forceSaveReminders = true;
} else {
checkTimeDependentFields(originalModel, model, values, modifyWhich);
ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
}
}
// New Event or New Exception to an existing event
boolean newEvent = (eventIdIndex != -1);
ArrayList<ReminderEntry> originalReminders;
if (originalModel != null) {
originalReminders = originalModel.mReminders;
} else {
originalReminders = new ArrayList<ReminderEntry>();
}
if (newEvent) {
saveRemindersWithBackRef(ops, eventIdIndex, reminders, originalReminders,
forceSaveReminders);
} else if (uri != null) {
long eventId = ContentUris.parseId(uri);
saveReminders(ops, eventId, reminders, originalReminders, forceSaveReminders);
}
ContentProviderOperation.Builder b;
boolean hasAttendeeData = model.mHasAttendeeData;
if (hasAttendeeData && model.mOwnerAttendeeId == -1) {
// Organizer is not an attendee
String ownerEmail = model.mOwnerAccount;
if (model.mAttendeesList.size() != 0 && Utils.isValidEmail(ownerEmail)) {
// Add organizer as attendee since we got some attendees
values.clear();
values.put(Attendees.ATTENDEE_EMAIL, ownerEmail);
values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
if (newEvent) {
b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
.withValues(values);
b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
} else {
values.put(Attendees.EVENT_ID, model.mId);
b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
.withValues(values);
}
ops.add(b.build());
}
} else if (hasAttendeeData &&
model.mSelfAttendeeStatus != originalModel.mSelfAttendeeStatus &&
model.mOwnerAttendeeId != -1) {
if (DEBUG) {
Log.d(TAG, "Setting attendee status to " + model.mSelfAttendeeStatus);
}
Uri attUri = ContentUris.withAppendedId(Attendees.CONTENT_URI, model.mOwnerAttendeeId);
values.clear();
values.put(Attendees.ATTENDEE_STATUS, model.mSelfAttendeeStatus);
values.put(Attendees.EVENT_ID, model.mId);
b = ContentProviderOperation.newUpdate(attUri).withValues(values);
ops.add(b.build());
}
// TODO: is this the right test? this currently checks if this is
// a new event or an existing event. or is this a paranoia check?
if (hasAttendeeData && (newEvent || uri != null)) {
String attendees = model.getAttendeesString();
String originalAttendeesString;
if (originalModel != null) {
originalAttendeesString = originalModel.getAttendeesString();
} else {
originalAttendeesString = "";
}
// Hit the content provider only if this is a new event or the user
// has changed it
if (newEvent || !TextUtils.equals(originalAttendeesString, attendees)) {
// figure out which attendees need to be added and which ones
// need to be deleted. use a linked hash set, so we maintain
// order (but also remove duplicates).
HashMap<String, Attendee> newAttendees = model.mAttendeesList;
LinkedList<String> removedAttendees = new LinkedList<String>();
// the eventId is only used if eventIdIndex is -1.
// TODO: clean up this code.
long eventId = uri != null ? ContentUris.parseId(uri) : -1;
// only compute deltas if this is an existing event.
// new events (being inserted into the Events table) won't
// have any existing attendees.
if (!newEvent) {
removedAttendees.clear();
HashMap<String, Attendee> originalAttendees = originalModel.mAttendeesList;
for (String originalEmail : originalAttendees.keySet()) {
if (newAttendees.containsKey(originalEmail)) {
// existing attendee. remove from new attendees set.
newAttendees.remove(originalEmail);
} else {
// no longer in attendees. mark as removed.
removedAttendees.add(originalEmail);
}
}
// delete removed attendees if necessary
if (removedAttendees.size() > 0) {
b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
String[] args = new String[removedAttendees.size() + 1];
args[0] = Long.toString(eventId);
int i = 1;
StringBuilder deleteWhere = new StringBuilder(ATTENDEES_DELETE_PREFIX);
for (String removedAttendee : removedAttendees) {
if (i > 1) {
deleteWhere.append(",");
}
deleteWhere.append("?");
args[i++] = removedAttendee;
}
deleteWhere.append(")");
b.withSelection(deleteWhere.toString(), args);
ops.add(b.build());
}
}
if (newAttendees.size() > 0) {
// Insert the new attendees
for (Attendee attendee : newAttendees.values()) {
values.clear();
values.put(Attendees.ATTENDEE_NAME, attendee.mName);
values.put(Attendees.ATTENDEE_EMAIL, attendee.mEmail);
values.put(Attendees.ATTENDEE_RELATIONSHIP,
Attendees.RELATIONSHIP_ATTENDEE);
values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
if (newEvent) {
b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
.withValues(values);
b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
} else {
values.put(Attendees.EVENT_ID, eventId);
b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
.withValues(values);
}
ops.add(b.build());
}
}
}
}
mService.startBatch(mService.getNextToken(), null, android.provider.CalendarContract.AUTHORITY, ops,
Utils.UNDO_DELAY);
return true;
}
public static LinkedHashSet<Rfc822Token> getAddressesFromList(String list,
Rfc822Validator validator) {
LinkedHashSet<Rfc822Token> addresses = new LinkedHashSet<Rfc822Token>();
Rfc822Tokenizer.tokenize(list, addresses);
if (validator == null) {
return addresses;
}
// validate the emails, out of paranoia. they should already be
// validated on input, but drop any invalid emails just to be safe.
Iterator<Rfc822Token> addressIterator = addresses.iterator();
while (addressIterator.hasNext()) {
Rfc822Token address = addressIterator.next();
if (!validator.isValid(address.getAddress())) {
Log.v(TAG, "Dropping invalid attendee email address: " + address.getAddress());
addressIterator.remove();
}
}
return addresses;
}
/**
* When we aren't given an explicit start time, we default to the next
* upcoming half hour. So, for example, 5:01 -> 5:30, 5:30 -> 6:00, etc.
*
* @return a UTC time in milliseconds representing the next upcoming half
* hour
*/
protected long constructDefaultStartTime(long now) {
Time defaultStart = new Time();
defaultStart.set(now);
defaultStart.second = 0;
defaultStart.minute = 30;
long defaultStartMillis = defaultStart.toMillis(false);
if (now < defaultStartMillis) {
return defaultStartMillis;
} else {
return defaultStartMillis + 30 * DateUtils.MINUTE_IN_MILLIS;
}
}
/**
* When we aren't given an explicit end time, we default to an hour after
* the start time.
* @param startTime the start time
* @return a default end time
*/
protected long constructDefaultEndTime(long startTime) {
return startTime + DateUtils.HOUR_IN_MILLIS;
}
// TODO think about how useful this is. Probably check if our event has
// changed early on and either update all or nothing. Should still do the if
// MODIFY_ALL bit.
void checkTimeDependentFields(CalendarEventModel originalModel, CalendarEventModel model,
ContentValues values, int modifyWhich) {
long oldBegin = model.mOriginalStart;
long oldEnd = model.mOriginalEnd;
boolean oldAllDay = originalModel.mAllDay;
String oldRrule = originalModel.mRrule;
String oldTimezone = originalModel.mTimezone;
long newBegin = model.mStart;
long newEnd = model.mEnd;
boolean newAllDay = model.mAllDay;
String newRrule = model.mRrule;
String newTimezone = model.mTimezone;
// If none of the time-dependent fields changed, then remove them.
if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
&& TextUtils.equals(oldRrule, newRrule)
&& TextUtils.equals(oldTimezone, newTimezone)) {
values.remove(Events.DTSTART);
values.remove(Events.DTEND);
values.remove(Events.DURATION);
values.remove(Events.ALL_DAY);
values.remove(Events.RRULE);
values.remove(Events.EVENT_TIMEZONE);
return;
}
if (TextUtils.isEmpty(oldRrule) || TextUtils.isEmpty(newRrule)) {
return;
}
// If we are modifying all events then we need to set DTSTART to the
// start time of the first event in the series, not the current
// date and time. If the start time of the event was changed
// (from, say, 3pm to 4pm), then we want to add the time difference
// to the start time of the first event in the series (the DTSTART
// value). If we are modifying one instance or all following instances,
// then we leave the DTSTART field alone.
if (modifyWhich == MODIFY_ALL) {
long oldStartMillis = originalModel.mStart;
if (oldBegin != newBegin) {
// The user changed the start time of this event
long offset = newBegin - oldBegin;
oldStartMillis += offset;
}
if (newAllDay) {
Time time = new Time(Time.TIMEZONE_UTC);
time.set(oldStartMillis);
time.hour = 0;
time.minute = 0;
time.second = 0;
oldStartMillis = time.toMillis(false);
}
values.put(Events.DTSTART, oldStartMillis);
}
}
/**
* Prepares an update to the original event so it stops where the new series
* begins. When we update 'this and all following' events we need to change
* the original event to end before a new series starts. This creates an
* update to the old event's rrule to do that.
*<p>
* If the event's recurrence rule has a COUNT, we also need to reduce the count in the
* RRULE for the exception event.
*
* @param ops The list of operations to add the update to
* @param originalModel The original event that we're updating
* @param endTimeMillis The time before which the event must end (i.e. the start time of the
* exception event instance).
* @return A replacement exception recurrence rule.
*/
public String updatePastEvents(ArrayList<ContentProviderOperation> ops,
CalendarEventModel originalModel, long endTimeMillis) {
boolean origAllDay = originalModel.mAllDay;
String origRrule = originalModel.mRrule;
String newRrule = origRrule;
EventRecurrence origRecurrence = new EventRecurrence();
origRecurrence.parse(origRrule);
// Get the start time of the first instance in the original recurrence.
long startTimeMillis = originalModel.mStart;
Time dtstart = new Time();
dtstart.timezone = originalModel.mTimezone;
dtstart.set(startTimeMillis);
ContentValues updateValues = new ContentValues();
if (origRecurrence.count > 0) {
/*
* Generate the full set of instances for this recurrence, from the first to the
* one just before endTimeMillis. The list should never be empty, because this method
* should not be called for the first instance. All we're really interested in is
* the *number* of instances found.
*
* TODO: the model assumes RRULE and ignores RDATE, EXRULE, and EXDATE. For the
* current environment this is reasonable, but that may not hold in the future.
*
* TODO: if COUNT is 1, should we convert the event to non-recurring? e.g. we
* do an "edit this and all future events" on the 2nd instances.
*/
RecurrenceSet recurSet = new RecurrenceSet(originalModel.mRrule, null, null, null);
RecurrenceProcessor recurProc = new RecurrenceProcessor();
long[] recurrences;
try {
recurrences = recurProc.expand(dtstart, recurSet, startTimeMillis, endTimeMillis);
} catch (DateException de) {
throw new RuntimeException(de);
}
if (recurrences.length == 0) {
throw new RuntimeException("can't use this method on first instance");
}
EventRecurrence excepRecurrence = new EventRecurrence();
excepRecurrence.parse(origRrule); // TODO: add+use a copy constructor instead
excepRecurrence.count -= recurrences.length;
newRrule = excepRecurrence.toString();
origRecurrence.count = recurrences.length;
} else {
// The "until" time must be in UTC time in order for Google calendar
// to display it properly. For all-day events, the "until" time string
// must include just the date field, and not the time field. The
// repeating events repeat up to and including the "until" time.
Time untilTime = new Time();
untilTime.timezone = Time.TIMEZONE_UTC;
// Subtract one second from the old begin time to get the new
// "until" time.
untilTime.set(endTimeMillis - 1000); // subtract one second (1000 millis)
if (origAllDay) {
untilTime.hour = 0;
untilTime.minute = 0;
untilTime.second = 0;
untilTime.allDay = true;
untilTime.normalize(false);
// This should no longer be necessary -- DTSTART should already be in the correct
// format for an all-day event.
dtstart.hour = 0;
dtstart.minute = 0;
dtstart.second = 0;
dtstart.allDay = true;
dtstart.timezone = Time.TIMEZONE_UTC;
}
origRecurrence.until = untilTime.format2445();
}
updateValues.put(Events.RRULE, origRecurrence.toString());
updateValues.put(Events.DTSTART, dtstart.normalize(true));
ContentProviderOperation.Builder b =
ContentProviderOperation.newUpdate(Uri.parse(originalModel.mUri))
.withValues(updateValues);
ops.add(b.build());
return newRrule;
}
/**
* Compares two models to ensure that they refer to the same event. This is
* a safety check to make sure an updated event model refers to the same
* event as the original model. If the original model is null then this is a
* new event or we're forcing an overwrite so we return true in that case.
* The important identifiers are the Calendar Id and the Event Id.
*
* @return
*/
public static boolean isSameEvent(CalendarEventModel model, CalendarEventModel originalModel) {
if (originalModel == null) {
return true;
}
if (model.mCalendarId != originalModel.mCalendarId) {
return false;
}
if (model.mId != originalModel.mId) {
return false;
}
return true;
}
/**
* Saves the reminders, if they changed. Returns true if operations to
* update the database were added.
*
* @param ops the array of ContentProviderOperations
* @param eventId the id of the event whose reminders are being updated
* @param reminders the array of reminders set by the user
* @param originalReminders the original array of reminders
* @param forceSave if true, then save the reminders even if they didn't change
* @return true if operations to update the database were added
*/
public static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
ArrayList<ReminderEntry> reminders, ArrayList<ReminderEntry> originalReminders,
boolean forceSave) {
// If the reminders have not changed, then don't update the database
if (reminders.equals(originalReminders) && !forceSave) {
return false;
}
// Delete all the existing reminders for this event
String where = Reminders.EVENT_ID + "=?";
String[] args = new String[] {Long.toString(eventId)};
ContentProviderOperation.Builder b = ContentProviderOperation
.newDelete(Reminders.CONTENT_URI);
b.withSelection(where, args);
ops.add(b.build());
ContentValues values = new ContentValues();
int len = reminders.size();
// Insert the new reminders, if any
for (int i = 0; i < len; i++) {
ReminderEntry re = reminders.get(i);
values.clear();
values.put(Reminders.MINUTES, re.getMinutes());
values.put(Reminders.METHOD, re.getMethod());
values.put(Reminders.EVENT_ID, eventId);
b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
ops.add(b.build());
}
return true;
}
/**
* Saves the reminders, if they changed. Returns true if operations to
* update the database were added. Uses a reference id since an id isn't
* created until the row is added.
*
* @param ops the array of ContentProviderOperations
* @param eventId the id of the event whose reminders are being updated
* @param reminderMinutes the array of reminders set by the user
* @param originalMinutes the original array of reminders
* @param forceSave if true, then save the reminders even if they didn't change
* @return true if operations to update the database were added
*/
public static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
int eventIdIndex, ArrayList<ReminderEntry> reminders,
ArrayList<ReminderEntry> originalReminders, boolean forceSave) {
// If the reminders have not changed, then don't update the database
if (reminders.equals(originalReminders) && !forceSave) {
return false;
}
// Delete all the existing reminders for this event
ContentProviderOperation.Builder b = ContentProviderOperation
.newDelete(Reminders.CONTENT_URI);
b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
b.withSelectionBackReference(0, eventIdIndex);
ops.add(b.build());
ContentValues values = new ContentValues();
int len = reminders.size();
// Insert the new reminders, if any
for (int i = 0; i < len; i++) {
ReminderEntry re = reminders.get(i);
values.clear();
values.put(Reminders.MINUTES, re.getMinutes());
values.put(Reminders.METHOD, re.getMethod());
b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
ops.add(b.build());
}
return true;
}
// It's the first event in the series if the start time before being
// modified is the same as the original event's start time
static boolean isFirstEventInSeries(CalendarEventModel model,
CalendarEventModel originalModel) {
return model.mOriginalStart == originalModel.mStart;
}
// Adds an rRule and duration to a set of content values
void addRecurrenceRule(ContentValues values, CalendarEventModel model) {
String rrule = model.mRrule;
values.put(Events.RRULE, rrule);
long end = model.mEnd;
long start = model.mStart;
String duration = model.mDuration;
boolean isAllDay = model.mAllDay;
if (end >= start) {
if (isAllDay) {
// if it's all day compute the duration in days
long days = (end - start + DateUtils.DAY_IN_MILLIS - 1)
/ DateUtils.DAY_IN_MILLIS;
duration = "P" + days + "D";
} else {
// otherwise compute the duration in seconds
long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
duration = "P" + seconds + "S";
}
} else if (TextUtils.isEmpty(duration)) {
// If no good duration info exists assume the default
if (isAllDay) {
duration = "P1D";
} else {
duration = "P3600S";
}
}
// recurring events should have a duration and dtend set to null
values.put(Events.DURATION, duration);
values.put(Events.DTEND, (Long) null);
}
/**
* Uses the recurrence selection and the model data to build an rrule and
* write it to the model.
*
* @param selection the type of rrule
* @param model The event to update
* @param weekStart the week start day, specified as java.util.Calendar
* constants
*/
static void updateRecurrenceRule(int selection, CalendarEventModel model,
int weekStart) {
// Make sure we don't have any leftover data from the previous setting
EventRecurrence eventRecurrence = new EventRecurrence();
if (selection == DOES_NOT_REPEAT) {
model.mRrule = null;
return;
} else if (selection == REPEATS_CUSTOM) {
// Keep custom recurrence as before.
return;
} else if (selection == REPEATS_DAILY) {
eventRecurrence.freq = EventRecurrence.DAILY;
} else if (selection == REPEATS_EVERY_WEEKDAY) {
eventRecurrence.freq = EventRecurrence.WEEKLY;
int dayCount = 5;
int[] byday = new int[dayCount];
int[] bydayNum = new int[dayCount];
byday[0] = EventRecurrence.MO;
byday[1] = EventRecurrence.TU;
byday[2] = EventRecurrence.WE;
byday[3] = EventRecurrence.TH;
byday[4] = EventRecurrence.FR;
for (int day = 0; day < dayCount; day++) {
bydayNum[day] = 0;
}
eventRecurrence.byday = byday;
eventRecurrence.bydayNum = bydayNum;
eventRecurrence.bydayCount = dayCount;
} else if (selection == REPEATS_WEEKLY_ON_DAY) {
eventRecurrence.freq = EventRecurrence.WEEKLY;
int[] days = new int[1];
int dayCount = 1;
int[] dayNum = new int[dayCount];
Time startTime = new Time(model.mTimezone);
startTime.set(model.mStart);
days[0] = EventRecurrence.timeDay2Day(startTime.weekDay);
// not sure why this needs to be zero, but set it for now.
dayNum[0] = 0;
eventRecurrence.byday = days;
eventRecurrence.bydayNum = dayNum;
eventRecurrence.bydayCount = dayCount;
} else if (selection == REPEATS_MONTHLY_ON_DAY) {
eventRecurrence.freq = EventRecurrence.MONTHLY;
eventRecurrence.bydayCount = 0;
eventRecurrence.bymonthdayCount = 1;
int[] bymonthday = new int[1];
Time startTime = new Time(model.mTimezone);
startTime.set(model.mStart);
bymonthday[0] = startTime.monthDay;
eventRecurrence.bymonthday = bymonthday;
} else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
eventRecurrence.freq = EventRecurrence.MONTHLY;
eventRecurrence.bydayCount = 1;
eventRecurrence.bymonthdayCount = 0;
int[] byday = new int[1];
int[] bydayNum = new int[1];
Time startTime = new Time(model.mTimezone);
startTime.set(model.mStart);
// Compute the week number (for example, the "2nd" Monday)
int dayCount = 1 + ((startTime.monthDay - 1) / 7);
if (dayCount == 5) {
dayCount = -1;
}
bydayNum[0] = dayCount;
byday[0] = EventRecurrence.timeDay2Day(startTime.weekDay);
eventRecurrence.byday = byday;
eventRecurrence.bydayNum = bydayNum;
} else if (selection == REPEATS_YEARLY) {
eventRecurrence.freq = EventRecurrence.YEARLY;
}
// Set the week start day.
eventRecurrence.wkst = EventRecurrence.calendarDay2Day(weekStart);
model.mRrule = eventRecurrence.toString();
}
/**
* Uses an event cursor to fill in the given model This method assumes the
* cursor used {@link #EVENT_PROJECTION} as it's query projection. It uses
* the cursor to fill in the given model with all the information available.
*
* @param model The model to fill in
* @param cursor An event cursor that used {@link #EVENT_PROJECTION} for the query
*/
public static void setModelFromCursor(CalendarEventModel model, Cursor cursor) {
if (model == null || cursor == null || cursor.getCount() != 1) {
Log.wtf(TAG, "Attempted to build non-existent model or from an incorrect query.");
return;
}
model.clear();
cursor.moveToFirst();
model.mId = cursor.getInt(EVENT_INDEX_ID);
model.mTitle = cursor.getString(EVENT_INDEX_TITLE);
model.mDescription = cursor.getString(EVENT_INDEX_DESCRIPTION);
model.mLocation = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
model.mAllDay = cursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
model.mHasAlarm = cursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
model.mCalendarId = cursor.getInt(EVENT_INDEX_CALENDAR_ID);
model.mStart = cursor.getLong(EVENT_INDEX_DTSTART);
String tz = cursor.getString(EVENT_INDEX_TIMEZONE);
if (!TextUtils.isEmpty(tz)) {
model.mTimezone = tz;
}
String rRule = cursor.getString(EVENT_INDEX_RRULE);
model.mRrule = rRule;
model.mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
model.mAvailability = cursor.getInt(EVENT_INDEX_AVAILABILITY);
int accessLevel = cursor.getInt(EVENT_INDEX_ACCESS_LEVEL);
model.mOwnerAccount = cursor.getString(EVENT_INDEX_OWNER_ACCOUNT);
model.mHasAttendeeData = cursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
model.mOriginalSyncId = cursor.getString(EVENT_INDEX_ORIGINAL_SYNC_ID);
model.mOriginalId = cursor.getLong(EVENT_INDEX_ORIGINAL_ID);
model.mOrganizer = cursor.getString(EVENT_INDEX_ORGANIZER);
model.mIsOrganizer = model.mOwnerAccount.equalsIgnoreCase(model.mOrganizer);
model.mGuestsCanModify = cursor.getInt(EVENT_INDEX_GUESTS_CAN_MODIFY) != 0;
int rawEventColor;
if (cursor.isNull(EVENT_INDEX_EVENT_COLOR)) {
rawEventColor = cursor.getInt(EVENT_INDEX_CALENDAR_COLOR);
} else {
rawEventColor = cursor.getInt(EVENT_INDEX_EVENT_COLOR);
}
model.setEventColor(Utils.getDisplayColorFromColor(rawEventColor));
if (accessLevel > 0) {
// For now the array contains the values 0, 2, and 3. We subtract
// one to make it easier to handle in code as 0,1,2.
// Default (0), Private (1), Public (2)
accessLevel--;
}
model.mAccessLevel = accessLevel;
model.mEventStatus = cursor.getInt(EVENT_INDEX_EVENT_STATUS);
boolean hasRRule = !TextUtils.isEmpty(rRule);
// We expect only one of these, so ignore the other
if (hasRRule) {
model.mDuration = cursor.getString(EVENT_INDEX_DURATION);
} else {
model.mEnd = cursor.getLong(EVENT_INDEX_DTEND);
}
model.mModelUpdatedWithEventCursor = true;
}
/**
* Uses a calendar cursor to fill in the given model This method assumes the
* cursor used {@link #CALENDARS_PROJECTION} as it's query projection. It uses
* the cursor to fill in the given model with all the information available.
*
* @param model The model to fill in
* @param cursor An event cursor that used {@link #CALENDARS_PROJECTION} for the query
* @return returns true if model was updated with the info in the cursor.
*/
public static boolean setModelFromCalendarCursor(CalendarEventModel model, Cursor cursor) {
if (model == null || cursor == null) {
Log.wtf(TAG, "Attempted to build non-existent model or from an incorrect query.");
return false;
}
if (model.mCalendarId == -1) {
return false;
}
if (!model.mModelUpdatedWithEventCursor) {
Log.wtf(TAG,
"Can't update model with a Calendar cursor until it has seen an Event cursor.");
return false;
}
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
if (model.mCalendarId != cursor.getInt(CALENDARS_INDEX_ID)) {
continue;
}
model.mOrganizerCanRespond = cursor.getInt(CALENDARS_INDEX_CAN_ORGANIZER_RESPOND) != 0;
model.mCalendarAccessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
model.mCalendarDisplayName = cursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
model.setCalendarColor(Utils.getDisplayColorFromColor(
cursor.getInt(CALENDARS_INDEX_COLOR)));
model.mCalendarAccountName = cursor.getString(CALENDARS_INDEX_ACCOUNT_NAME);
model.mCalendarAccountType = cursor.getString(CALENDARS_INDEX_ACCOUNT_TYPE);
model.mCalendarMaxReminders = cursor.getInt(CALENDARS_INDEX_MAX_REMINDERS);
model.mCalendarAllowedReminders = cursor.getString(CALENDARS_INDEX_ALLOWED_REMINDERS);
model.mCalendarAllowedAttendeeTypes = cursor
.getString(CALENDARS_INDEX_ALLOWED_ATTENDEE_TYPES);
model.mCalendarAllowedAvailability = cursor
.getString(CALENDARS_INDEX_ALLOWED_AVAILABILITY);
return true;
}
return false;
}
public static boolean canModifyEvent(CalendarEventModel model) {
return canModifyCalendar(model)
&& (model.mIsOrganizer || model.mGuestsCanModify);
}
public static boolean canModifyCalendar(CalendarEventModel model) {
return model.mCalendarAccessLevel >= Calendars.CAL_ACCESS_CONTRIBUTOR
|| model.mCalendarId == -1;
}
public static boolean canAddReminders(CalendarEventModel model) {
return model.mCalendarAccessLevel >= Calendars.CAL_ACCESS_READ;
}
public static boolean canRespond(CalendarEventModel model) {
// For non-organizers, write permission to the calendar is sufficient.
// For organizers, the user needs a) write permission to the calendar
// AND b) ownerCanRespond == true AND c) attendee data exist
// (this means num of attendees > 1, the calendar owner's and others).
// Note that mAttendeeList omits the organizer.
// (there are more cases involved to be 100% accurate, such as
// paying attention to whether or not an attendee status was
// included in the feed, but we're currently omitting those corner cases
// for simplicity).
if (!canModifyCalendar(model)) {
return false;
}
if (!model.mIsOrganizer) {
return true;
}
if (!model.mOrganizerCanRespond) {
return false;
}
// This means we don't have the attendees data so we can't send
// the list of attendees and the status back to the server
if (model.mHasAttendeeData && model.mAttendeesList.size() == 0) {
return false;
}
return true;
}
/**
* Goes through an event model and fills in content values for saving. This
* method will perform the initial collection of values from the model and
* put them into a set of ContentValues. It performs some basic work such as
* fixing the time on allDay events and choosing whether to use an rrule or
* dtend.
*
* @param model The complete model of the event you want to save
* @return values
*/
ContentValues getContentValuesFromModel(CalendarEventModel model) {
String title = model.mTitle;
boolean isAllDay = model.mAllDay;
String rrule = model.mRrule;
String timezone = model.mTimezone;
if (timezone == null) {
timezone = TimeZone.getDefault().getID();
}
Time startTime = new Time(timezone);
Time endTime = new Time(timezone);
startTime.set(model.mStart);
endTime.set(model.mEnd);
offsetStartTimeIfNecessary(startTime, endTime, rrule, model);
ContentValues values = new ContentValues();
long startMillis;
long endMillis;
long calendarId = model.mCalendarId;
if (isAllDay) {
// Reset start and end time, ensure at least 1 day duration, and set
// the timezone to UTC, as required for all-day events.
timezone = Time.TIMEZONE_UTC;
startTime.hour = 0;
startTime.minute = 0;
startTime.second = 0;
startTime.timezone = timezone;
startMillis = startTime.normalize(true);
endTime.hour = 0;
endTime.minute = 0;
endTime.second = 0;
endTime.timezone = timezone;
endMillis = endTime.normalize(true);
if (endMillis < startMillis + DateUtils.DAY_IN_MILLIS) {
// EditEventView#fillModelFromUI() should treat this case, but we want to ensure
// the condition anyway.
endMillis = startMillis + DateUtils.DAY_IN_MILLIS;
}
} else {
startMillis = startTime.toMillis(true);
endMillis = endTime.toMillis(true);
}
values.put(Events.CALENDAR_ID, calendarId);
values.put(Events.EVENT_TIMEZONE, timezone);
values.put(Events.TITLE, title);
values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
values.put(Events.DTSTART, startMillis);
values.put(Events.RRULE, rrule);
if (!TextUtils.isEmpty(rrule)) {
addRecurrenceRule(values, model);
} else {
values.put(Events.DURATION, (String) null);
values.put(Events.DTEND, endMillis);
}
if (model.mDescription != null) {
values.put(Events.DESCRIPTION, model.mDescription.trim());
} else {
values.put(Events.DESCRIPTION, (String) null);
}
if (model.mLocation != null) {
values.put(Events.EVENT_LOCATION, model.mLocation.trim());
} else {
values.put(Events.EVENT_LOCATION, (String) null);
}
values.put(Events.AVAILABILITY, model.mAvailability);
values.put(Events.HAS_ATTENDEE_DATA, model.mHasAttendeeData ? 1 : 0);
int accessLevel = model.mAccessLevel;
if (accessLevel > 0) {
// For now the array contains the values 0, 2, and 3. We add one to match.
// Default (0), Private (2), Public (3)
accessLevel++;
}
values.put(Events.ACCESS_LEVEL, accessLevel);
values.put(Events.STATUS, model.mEventStatus);
if (model.isEventColorInitialized()) {
if (model.getEventColor() == model.getCalendarColor()) {
values.put(Events.EVENT_COLOR_KEY, NO_EVENT_COLOR);
} else {
values.put(Events.EVENT_COLOR_KEY, model.getEventColorKey());
}
}
return values;
}
/**
* If the recurrence rule is such that the event start date doesn't actually fall in one of the
* recurrences, then push the start date up to the first actual instance of the event.
*/
private void offsetStartTimeIfNecessary(Time startTime, Time endTime, String rrule,
CalendarEventModel model) {
if (rrule == null || rrule.isEmpty()) {
// No need to waste any time with the parsing if the rule is empty.
return;
}
mEventRecurrence.parse(rrule);
// Check if we meet the specific special case. It has to:
// * be weekly
// * not recur on the same day of the week that the startTime falls on
// In this case, we'll need to push the start time to fall on the first day of the week
// that is part of the recurrence.
if (mEventRecurrence.freq != EventRecurrence.WEEKLY) {
// Not weekly so nothing to worry about.
return;
}
if (mEventRecurrence.byday == null ||
mEventRecurrence.byday.length > mEventRecurrence.bydayCount) {
// This shouldn't happen, but just in case something is weird about the recurrence.
return;
}
// Start to figure out what the nearest weekday is.
int closestWeekday = Integer.MAX_VALUE;
int weekstart = EventRecurrence.day2TimeDay(mEventRecurrence.wkst);
int startDay = startTime.weekDay;
for (int i = 0; i < mEventRecurrence.bydayCount; i++) {
int day = EventRecurrence.day2TimeDay(mEventRecurrence.byday[i]);
if (day == startDay) {
// Our start day is one of the recurring days, so we're good.
return;
}
if (day < weekstart) {
// Let's not make any assumptions about what weekstart can be.
day += 7;
}
// We either want the earliest day that is later in the week than startDay ...
if (day > startDay && (day < closestWeekday || closestWeekday < startDay)) {
closestWeekday = day;
}
// ... or if there are no days later than startDay, we want the earliest day that is
// earlier in the week than startDay.
if (closestWeekday == Integer.MAX_VALUE || closestWeekday < startDay) {
// We haven't found a day that's later in the week than startDay yet.
if (day < closestWeekday) {
closestWeekday = day;
}
}
}
// We're here, so unfortunately our event's start day is not included in the days of
// the week of the recurrence. To save this event correctly we'll need to push the start
// date to the closest weekday that *is* part of the recurrence.
if (closestWeekday < startDay) {
closestWeekday += 7;
}
int daysOffset = closestWeekday - startDay;
startTime.monthDay += daysOffset;
endTime.monthDay += daysOffset;
long newStartTime = startTime.normalize(true);
long newEndTime = endTime.normalize(true);
// Later we'll actually be using the values from the model rather than the startTime
// and endTime themselves, so we need to make these changes to the model as well.
model.mStart = newStartTime;
model.mEnd = newEndTime;
}
/**
* Takes an e-mail address and returns the domain (everything after the last @)
*/
public static String extractDomain(String email) {
int separator = email.lastIndexOf('@');
if (separator != -1 && ++separator < email.length()) {
return email.substring(separator);
}
return null;
}
public interface EditDoneRunnable extends Runnable {
public void setDoneCode(int code);
}
}