Skip to content

Commit

Permalink
handle recurring event exceptions correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-haeusler committed Nov 13, 2023
1 parent 0854c06 commit b1931c0
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 82 deletions.
73 changes: 21 additions & 52 deletions app/src/main/java/com/android/calendar/DeleteEventHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,25 @@ public class DeleteEventHelper {
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int button) {
deleteStarted();
deleteNormalEvent();
long id = mModel.mId; // mCursor.getInt(mEventIndexId);

// If this event is part of a local calendar, really remove it from the database
//
// "There are two versions of delete: as an app and as a sync adapter.
// An app delete will set the deleted column on an event and remove all instances of that event.
// A sync adapter delete will remove the event from the database and all associated data."
// from https://developer.android.com/reference/android/provider/CalendarContract.Events
boolean isLocal = mModel.mSyncAccountType.equals(CalendarContract.ACCOUNT_TYPE_LOCAL);
Uri deleteContentUri = isLocal ? CalendarRepository.asLocalCalendarSyncAdapter(mModel.mSyncAccountName, Events.CONTENT_URI) : Events.CONTENT_URI;

Uri uri = ContentUris.withAppendedId(deleteContentUri, id);
mService.startDelete(mService.getNextToken(), null, uri, null, null, Utils.UNDO_DELAY);
if (mCallback != null) {
mCallback.run();
}
if (mExitWhenDone) {
mParent.finish();
}
}
};
/**
Expand Down Expand Up @@ -146,7 +164,7 @@ public void onClick(DialogInterface dialog, int button) {
}
};

public DeleteEventHelper(Context context, Activity parentActivity, boolean exitWhenDone, boolean prompt) {
public DeleteEventHelper(Context context, Activity parentActivity, boolean exitWhenDone) {
if (exitWhenDone && parentActivity == null) {
throw new IllegalArgumentException("parentActivity is required to exit when done");
}
Expand All @@ -164,20 +182,12 @@ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
CalendarEventModel mModel = new CalendarEventModel();
EditEventHelper.setModelFromCursor(mModel, cursor, mContext);
cursor.close();
if (prompt) {
delete(mStartMillis, mEndMillis, mModel, mWhichDelete);
} else {
deleteUnprompted(mStartMillis, mEndMillis, mModel, mWhichDelete);
}
DeleteEventHelper.this.delete(mStartMillis, mEndMillis, mModel, mWhichDelete);
}
};
mExitWhenDone = exitWhenDone;
}

public DeleteEventHelper(Context context, Activity parentActivity, boolean exitWhenDone) {
this(context, parentActivity, exitWhenDone, true);
}

public void setExitWhenDone(boolean exitWhenDone) {
mExitWhenDone = exitWhenDone;
}
Expand Down Expand Up @@ -329,47 +339,6 @@ public void delete(long begin, long end, CalendarEventModel model, int which) {
}
}

private void deleteUnprompted(long begin, long end, CalendarEventModel model, int which) {
mWhichDelete = which;
mStartMillis = begin;
mEndMillis = end;
mModel = model;
mSyncId = model.mSyncId;

if (TextUtils.isEmpty(model.mRrule)) {
String originalEvent = model.mOriginalSyncId;
if (originalEvent == null) {
deleteNormalEvent();
} else {
deleteExceptionEvent();
}
} else {
deleteRepeatingEvent(which);
}
}

private void deleteNormalEvent() {
long id = mModel.mId; // mCursor.getInt(mEventIndexId);

// If this event is part of a local calendar, really remove it from the database
//
// "There are two versions of delete: as an app and as a sync adapter.
// An app delete will set the deleted column on an event and remove all instances of that event.
// A sync adapter delete will remove the event from the database and all associated data."
// from https://developer.android.com/reference/android/provider/CalendarContract.Events
boolean isLocal = mModel.mSyncAccountType.equals(CalendarContract.ACCOUNT_TYPE_LOCAL);
Uri deleteContentUri = isLocal ? CalendarRepository.asLocalCalendarSyncAdapter(mModel.mSyncAccountName, Events.CONTENT_URI) : Events.CONTENT_URI;

Uri uri = ContentUris.withAppendedId(deleteContentUri, id);
mService.startDelete(mService.getNextToken(), null, uri, null, null, Utils.UNDO_DELAY);
if (mCallback != null) {
mCallback.run();
}
if (mExitWhenDone) {
mParent.finish();
}
}

private void deleteExceptionEvent() {
long id = mModel.mId; // mCursor.getInt(mEventIndexId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
long eventId;
switch (token) {
case TOKEN_EVENT:
if (cursor.getCount() == 0) {
if (!cursor.moveToFirst()) {
// The cursor is empty. This can happen if the event
// was deleted.
cursor.close();
Expand Down
108 changes: 89 additions & 19 deletions app/src/main/java/com/android/calendar/event/EditEventHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.android.calendar.event;

import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
Expand All @@ -41,7 +42,6 @@
import com.android.calendar.CalendarEventModel;
import com.android.calendar.CalendarEventModel.Attendee;
import com.android.calendar.CalendarEventModel.ReminderEntry;
import com.android.calendar.DeleteEventHelper;
import com.android.calendar.Utils;
import com.android.calendarcommon2.DateException;
import com.android.calendarcommon2.EventRecurrence;
Expand All @@ -55,6 +55,7 @@
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

public class EditEventHelper {
Expand Down Expand Up @@ -94,7 +95,8 @@ public class EditEventHelper {
Events.EVENT_COLOR, // 23
Events.EVENT_COLOR_KEY, // 24
Events.ACCOUNT_NAME, // 25
Events.ACCOUNT_TYPE // 26
Events.ACCOUNT_TYPE, // 26
Events.ORIGINAL_INSTANCE_TIME // 28
};
protected static final int EVENT_INDEX_ID = 0;
protected static final int EVENT_INDEX_TITLE = 1;
Expand Down Expand Up @@ -123,6 +125,7 @@ public class EditEventHelper {
protected static final int EVENT_INDEX_EVENT_COLOR_KEY = 24;
protected static final int EVENT_INDEX_ACCOUNT_NAME = 25;
protected static final int EVENT_INDEX_ACCOUNT_TYPE = 26;
protected static final int EVENT_INDEX_ORIGINAL_INSTANCE_TIME = 27;

public static final String[] REMINDERS_PROJECTION = new String[] {
Reminders._ID, // 0
Expand Down Expand Up @@ -158,7 +161,8 @@ public class EditEventHelper {

private final AsyncQueryService mService;

private final DeleteEventHelper deleteEventHelper;
private final ContentResolver mContextResolver;
private final Context mContext;

// 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.
Expand Down Expand Up @@ -269,7 +273,8 @@ public AttendeeItem(Attendee attendee, Drawable badge) {

public EditEventHelper(Context context) {
mService = ((AbstractCalendarActivity)context).getAsyncQueryService();
deleteEventHelper = new DeleteEventHelper(context, null, false, false);
mContextResolver = context.getContentResolver();
this.mContext = context;
}

/**
Expand Down Expand Up @@ -315,19 +320,6 @@ public boolean saveEvent(CalendarEventModel model, CalendarEventModel originalMo
return false;
}

// check if the event calendar changed
if (originalModel != null && originalModel.mCalendarId != model.mCalendarId) {
// delete event from original calendar and recreate in new calendar with new id
deleteEventHelper.delete(model.mStart, model.mEnd, model.mId, modifyWhich);

model.mId = -1;
model.mUri = null;
model.mSyncId = null;
model.mOriginalSyncId = null;
model.mSyncAccountName = null;
model.mSyncAccountType = null;
}

ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
int eventIdIndex = -1;

Expand Down Expand Up @@ -357,6 +349,15 @@ public boolean saveEvent(CalendarEventModel model, CalendarEventModel originalMo
ops.add(b.build());
forceSaveReminders = true;

} else if (originalModel.mCalendarId != model.mCalendarId) {
// event calendar has changed
eventIdIndex = ops.size();

ops.addAll(moveEventToCalendar(
String.valueOf(model.mId),
String.valueOf(model.mCalendarId)));

forceSaveReminders = true;
} else if (TextUtils.isEmpty(model.mRrule) && TextUtils.isEmpty(originalModel.mRrule)) {
// Simple update to a non-recurring event
checkTimeDependentFields(originalModel, model, values, modifyWhich);
Expand Down Expand Up @@ -617,6 +618,74 @@ public boolean saveEvent(CalendarEventModel model, CalendarEventModel originalMo
return true;
}

private List<ContentProviderOperation> moveEventToCalendar(
final String eventId, final String newCalendarId) {

final List<ContentProviderOperation> ops = new ArrayList<>();
final String syncId;
int eventIdIndex;

// retrieve event, create it in target calendar, delete from source calendar
try (Cursor cursor = mContextResolver.query(
Events.CONTENT_URI,
EVENT_PROJECTION,
Events._ID + "= ?",
new String[]{String.valueOf(eventId)},
null
)) {
if (cursor == null || cursor.getCount() != 1 || !cursor.moveToFirst()) {
final String count = cursor == null ? "no cursor" : String.valueOf(cursor.getCount());
throw new IllegalStateException("expected exactly 1 event, but got " + count);
}

final CalendarEventModel model = new CalendarEventModel();
setModelFromCursor(model, cursor, mContext);
final ContentValues values = getContentValuesFromModel(model);
values.put(Events.CALENDAR_ID, newCalendarId);
syncId = model.mSyncId;

// set eventIdIndex for back referencing when inserting exceptions
eventIdIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI)
.withValues(values)
.build());

ops.add(ContentProviderOperation.newDelete(
ContentUris.withAppendedId(Events.CONTENT_URI, model.mId)).build());
}

// retrieve events exceptions, create them in target calendar, delete them from source calendar
try (Cursor cursor = mContextResolver.query(
Events.CONTENT_URI,
EVENT_PROJECTION,
Events.ORIGINAL_SYNC_ID + "= ? AND " + Events._SYNC_ID + " IS NULL",
new String[]{String.valueOf(syncId)},
null
)) {
while (cursor != null && cursor.moveToNext()) {
final CalendarEventModel model = new CalendarEventModel();
setModelFromCursor(model, cursor, mContext);
final ContentValues values = getContentValuesFromModel(model);
values.put(Events.CALENDAR_ID, newCalendarId);
values.put(Events.ORIGINAL_INSTANCE_TIME, model.mOriginalTime);

ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI)
.withValues(values)
// TODO: Call requires API level 30
.withValueBackReference(Events.ORIGINAL_ID, eventIdIndex, Events._ID)
.build());

Uri.Builder b = Events.CONTENT_EXCEPTION_URI.buildUpon();
ContentUris.appendId(b, Long.parseLong(eventId));
ContentUris.appendId(b, model.mId);

ops.add(ContentProviderOperation.newDelete(b.build()).build());
}
}

return ops;
}

public static LinkedHashSet<Rfc822Token> getAddressesFromList(String list,
Rfc822Validator validator) {
LinkedHashSet<Rfc822Token> addresses = new LinkedHashSet<Rfc822Token>();
Expand Down Expand Up @@ -1057,18 +1126,18 @@ static void updateRecurrenceRule(int selection, CalendarEventModel model,
* 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.
* Only the row the cursor currently points to is used.
*
* @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, Context context) {
if (model == null || cursor == null || cursor.getCount() != 1) {
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);
Expand Down Expand Up @@ -1096,6 +1165,7 @@ public static void setModelFromCursor(CalendarEventModel model, Cursor cursor, C
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.mOriginalTime = cursor.getLong(EVENT_INDEX_ORIGINAL_INSTANCE_TIME);
model.mOrganizer = cursor.getString(EVENT_INDEX_ORGANIZER);
model.mIsOrganizer = model.mOwnerAccount.equalsIgnoreCase(model.mOrganizer);
model.mGuestsCanModify = cursor.getInt(EVENT_INDEX_GUESTS_CAN_MODIFY) != 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,7 @@ private void setViewStates(int mode) {
if (TextUtils.isEmpty(mUrlTextView.getText())) {
mUrlGroup.setVisibility(View.GONE);
}
mCalendarsSpinner.setEnabled(false);
} else {
for (View v : mViewOnlyList) {
v.setVisibility(View.GONE);
Expand All @@ -1223,6 +1224,7 @@ private void setViewStates(int mode) {
mLocationGroup.setVisibility(View.VISIBLE);
mDescriptionGroup.setVisibility(View.VISIBLE);
mUrlGroup.setVisibility(View.VISIBLE);
mCalendarsSpinner.setEnabled(mode == Utils.MODIFY_ALL);
}
setAllDayViewsVisibility(mAllDayCheckBox.isChecked());
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/color/calendar_spinner_item_colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#61000000" android:state_enabled="false" />
<item android:color="#000000" />
</selector>
20 changes: 10 additions & 10 deletions app/src/main/res/layout/calendars_spinner_item.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
<?xml version="1.0" encoding="utf-8"?><!-- 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.
Expand All @@ -15,27 +14,28 @@
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_width="match_parent">
android:orientation="vertical">

<TextView android:id="@+id/calendar_name"
style="?android:attr/spinnerItemStyle"
<TextView
android:id="@+id/calendar_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:duplicateParentState="true"
android:ellipsize="end"
android:textColor="?attr/light_dark"
android:singleLine="true"
android:textColor="@color/calendar_spinner_item_colors"
android:textSize="18sp" />

<TextView
android:id="@+id/account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:ellipsize="marquee"
android:singleLine="true"
style="?android:attr/spinnerItemStyle"
android:textColor="?attr/light_dark"
android:textColor="@color/calendar_spinner_item_colors"
android:textSize="14sp" />

</LinearLayout>
1 change: 1 addition & 0 deletions app/src/main/res/values/attrs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
<attr name="iconAddCalendar" format="reference" />
<attr name="iconCalendarAccount" format="reference" />
<attr name="divider_select_calendars" format="color"/>
<attr name="calendarSpinnerDisabledTextColor" format="color" />
</declare-styleable>
</resources>
Loading

0 comments on commit b1931c0

Please sign in to comment.