Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 0a58401c authored by Jonas Häusler's avatar Jonas Häusler Committed by Michael Bestas
Browse files

handle recurring event exceptions correctly

Change-Id: I31fb0012bf293d6adee59311a26019b619ca5253
parent 09a250c5
Loading
Loading
Loading
Loading
+21 −52
Original line number Diff line number Diff line
@@ -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();
            }
        }
    };
    /**
@@ -146,7 +164,7 @@ public class DeleteEventHelper {
        }
    };

    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");
        }
@@ -164,20 +182,12 @@ public class DeleteEventHelper {
                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;
    }
@@ -329,47 +339,6 @@ public class DeleteEventHelper {
        }
    }

    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);

+1 −1
Original line number Diff line number Diff line
@@ -688,7 +688,7 @@ public class EditEventFragment extends Fragment implements EventHandler, OnColor
            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();
+89 −19
Original line number Diff line number Diff line
@@ -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;
@@ -41,7 +42,6 @@ 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.DeleteEventHelper;
import com.android.calendar.Utils;
import com.android.calendarcommon2.DateException;
import com.android.calendarcommon2.EventRecurrence;
@@ -55,6 +55,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

public class EditEventHelper {
@@ -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;
@@ -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
@@ -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.
@@ -269,7 +273,8 @@ public class EditEventHelper {

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

    /**
@@ -315,19 +320,6 @@ public class EditEventHelper {
            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;

@@ -357,6 +349,15 @@ public class EditEventHelper {
            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);
@@ -617,6 +618,74 @@ public class EditEventHelper {
        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>();
@@ -1057,18 +1126,18 @@ public class EditEventHelper {
     * 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);
@@ -1096,6 +1165,7 @@ public class EditEventHelper {
        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;
+2 −0
Original line number Diff line number Diff line
@@ -1197,6 +1197,7 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa
            if (TextUtils.isEmpty(mUrlTextView.getText())) {
                mUrlGroup.setVisibility(View.GONE);
            }
            mCalendarsSpinner.setEnabled(false);
        } else {
            for (View v : mViewOnlyList) {
                v.setVisibility(View.GONE);
@@ -1223,6 +1224,7 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa
            mLocationGroup.setVisibility(View.VISIBLE);
            mDescriptionGroup.setVisibility(View.VISIBLE);
            mUrlGroup.setVisibility(View.VISIBLE);
            mCalendarsSpinner.setEnabled(mode == Utils.MODIFY_ALL);
        }
        setAllDayViewsVisibility(mAllDayCheckBox.isChecked());
    }
+5 −0
Original line number Diff line number Diff line
<?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>
Loading