关于google Calendar Instance 的删除问题

时间: 2023-10-04 admin IT培训

关于google Calendar Instance 的删除问题

关于google Calendar Instance 的删除问题

1.问题背景:最近遇到一个问题,就是要删除Calendar Event 的Instance,但是在看官方Api,发现 google并没有提供删除Instance的接口,也就是,你只能差而不能删除Instance

链接如下:=zh-cn

关系图如下:

 

从图中我们看到每个Event中对应多个Instances,(所谓的Instance就是每个事件) ,比如,创建一个日常重复的事件,那么就会产生一个Event和无数个Instance

思考:

1.既然不能从Insance去入手删除,那只能从Event入手了,既然系统能删除单个Instance,那一定有一个触发的时机

2.我们在定义重复的repeat Event的时候会去设置rRule,也就是重复规则

3.在Event 中有一个ExDate字段,也就是在设置重复规则的时候会,根据ExDate去将字段中的日期排除

4.那就是说我们只要设置ExDate,就能够排出掉我们想要删除的ExDate。

尝试:

1.直接去Update ExDate

 ContentResolver resolver = context.getContentResolver();Uri eventUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);values.put(CalendarContract.Events.EXDATE, resultExDate.toString());

发现没有作用。当时做到这边的时候感觉一脸懵逼,觉得官方在骗我,这个根本没用!。

2.后来继续查找资料,发现说ExDate 要跟 DTStart还有RRule一起使用

思考:

1.也就是说单纯设置ExDate的话没办法触发事件的变化

2.那设置DTStart 和Rrule会有什么效果

于是我做了以下尝试:

1.单独更新DTStart

 ContentResolver resolver = context.getContentResolver();Uri eventUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);values.put(CalendarContract.Events.DTSTART, dStart);

 

结果:之前的重复事件全部没有了!只剩下当前的startTime的事件‘

2.单独设置rRule,也就是重复规则

  ContentResolver resolver = context.getContentResolver();Uri eventUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);
values.put(CalendarContract.Events.RRULE, rRule);

结果没有效果

3.同时设置DTstart和RRule

结果:生成了一些列重复事件。

思考:

1.从上面的尝试得出结论,DTSTart是触发更新的必要元素

2.单纯设了DTSTart之前的事件会被删除,说明更新后,会有一个删除旧数据然后重新创建的过程

3.结合以上几点,只要同时设置三个参数 ,应该就能达到我们想要的效果,也就是重新制定所有的规则,让他重新生成一些列事件。

尝试:

   ContentResolver resolver = context.getContentResolver();Uri eventUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);ContentValues values = new ContentValues();values.put(CalendarContract.Events.DTSTART, dStart);values.put(CalendarContract.Events.EXDATE, resultExDate.toString());values.put(CalendarContract.Events.RRULE, rRule);

结果:这个时候还是不行!

不对一定是写代码的姿势不够帅!!!!

继续找答案:发现了这个东西

DtStartTime的格式,发现用的是UTC时间格式,

格式化方式如下:

 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.getDefault());simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

然后再跑一下。

结果:可行!!!!!!

无敌!!!我真的是无敌,还有谁!!!!!!!

经过一些列的探索,解决了问题,于是我决定,将革命进行到底,去瞅瞅源码

源码分析:主要看CalendarProvider2.java RecurrenceSet  CalendarInstancesHelper  RecurrenceProcessor

链接如下:/+/8d2ed3bf1ef3525c3a6eb17b57f07b0af35ef4d0/src/com/android/providers/calendar/CalendarProvider2.java

/+/06b3293d5af3454a39681cfd659271551354b8a0/src/com/android/calendarcommon2/RecurrenceSet.java

/+/master/src/com/android/providers/calendar/CalendarInstancesHelper.java

/+/06b3293d5af3454a39681cfd659271551354b8a0/src/com/android/calendarcommon2/RecurrenceProcessor.java

1.首先从CalendarProvider如手

Provider看名字就知道是操作日历数据库,既然,我们调用的是update方法,那么我么直接定位到更新操作就可以了

private int handleUpdateEvents(Cursor cursor, ContentValues updateValues,boolean callerIsSyncAdapter) {/** This field is considered read-only.  It should not be modified by applications or* by the sync adapter.*/updateValues.remove(Events.HAS_ALARM);/** For a single event, we can just load the event, merge modValues in, perform any* fix-ups (putting changes into modValues), check validity, and then update().  We have* to be careful that our fix-ups don't confuse the sync adapter.** For multiple events, we need to load, merge, and validate each event individually.* If no single-event-specific changes need to be made, we could just issue the original* bulk update, which would be more efficient than a series of individual updates.* However, doing so would prevent us from taking advantage of the partial-update* mechanism.*/if (cursor.getCount() > 1) {if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "Performing update on " + cursor.getCount() + " events");}}while (cursor.moveToNext()) {// Make a copy of updateValues so we can make some local changes.ContentValues modValues = new ContentValues(updateValues);// Load the event into a ContentValues object.ContentValues values = new ContentValues();DatabaseUtils.cursorRowToContentValues(cursor, values);boolean doValidate = false;if (!callerIsSyncAdapter) {try {// Check to see if the data in the database is valid.  If not, we will skip// validation of the update, so that we don't blow up on attempts to// modify existing badly-formed events.validateEventData(values);doValidate = true;} catch (IllegalArgumentException iae) {Log.d(TAG, "Event " + values.getAsString(Events._ID) +" malformed, not validating update (" +iae.getMessage() + ")");}}// Merge the modifications in.values.putAll(modValues);// If a color_index is being set make sure it's validString color_id = modValues.getAsString(Events.EVENT_COLOR_KEY);if (!TextUtils.isEmpty(color_id)) {String accountName = null;String accountType = null;Cursor c = mDb.query(Tables.CALENDARS, ACCOUNT_PROJECTION, SQL_WHERE_ID,new String[] { values.getAsString(Events.CALENDAR_ID) }, null, null, null);try {if (c.moveToFirst()) {accountName = c.getString(ACCOUNT_NAME_INDEX);accountType = c.getString(ACCOUNT_TYPE_INDEX);}} finally {if (c != null) {c.close();}}verifyColorExists(accountName, accountType, color_id, Colors.TYPE_EVENT);}// Scrub and/or validate the combined event.if (callerIsSyncAdapter) {scrubEventData(values, modValues);}if (doValidate) {validateEventData(values);}// Look for any updates that could affect LAST_DATE.  It's defined as the end of// the last meeting, so we need to pay attention to DURATION.if (modValues.containsKey(Events.DTSTART) ||modValues.containsKey(Events.DTEND) ||modValues.containsKey(Events.DURATION) ||modValues.containsKey(Events.EVENT_TIMEZONE) ||modValues.containsKey(Events.RRULE) ||modValues.containsKey(Events.RDATE) ||modValues.containsKey(Events.EXRULE) ||modValues.containsKey(Events.EXDATE)) {long newLastDate;try {newLastDate = calculateLastDate(values);} catch (DateException de) {throw new IllegalArgumentException("Unable to compute LAST_DATE", de);}Long oldLastDateObj = values.getAsLong(Events.LAST_DATE);long oldLastDate = (oldLastDateObj == null) ? -1 : oldLastDateObj;if (oldLastDate != newLastDate) {// This overwrites any caller-supplied LAST_DATE.  This is okay, because the// caller isn't supposed to be messing with the LAST_DATE field.if (newLastDate < 0) {modValues.putNull(Events.LAST_DATE);} else {modValues.put(Events.LAST_DATE, newLastDate);}}}if (!callerIsSyncAdapter) {modValues.put(Events.DIRTY, 1);}// Disallow updating the attendee status in the Events// table.  In the future, we could support this but we// would have to query and update the attendees table// to keep the values consistent.if (modValues.containsKey(Events.SELF_ATTENDEE_STATUS)) {throw new IllegalArgumentException("Updating "+ Events.SELF_ATTENDEE_STATUS+ " in Events table is not allowed.");}if (fixAllDayTime(values, modValues)) {if (Log.isLoggable(TAG, Log.WARN)) {Log.w(TAG, "handleUpdateEvents: " +"allDay is true but sec, min, hour were not 0.");}}// For taking care about recurrences exceptions cancelations, check if this needs//  to be an UPDATE or a DELETEboolean isUpdate = doesStatusCancelUpdateMeanUpdate(values, modValues);long id = values.getAsLong(Events._ID);if (isUpdate) {// If a user made a change, possibly duplicate the event so we can do a partial// update. If a sync adapter made a change and that change marks an event as// un-dirty, remove any duplicates that may have been created earlier.if (!callerIsSyncAdapter) {mDbHelper.duplicateEvent(id);} else {if (modValues.containsKey(Events.DIRTY)&& modValues.getAsInteger(Events.DIRTY) == 0) {mDbHelper.removeDuplicateEvent(id);}}int result = mDb.update(Tables.EVENTS, modValues, SQL_WHERE_ID,new String[] { String.valueOf(id) });if (result > 0) {updateEventRawTimesLocked(id, modValues);mInstancesHelper.updateInstancesLocked(modValues, id,false /* not a new event */, mDb);// XXX: should we also be doing this when RRULE changes (e.g. instances//      are introduced or removed?)if (modValues.containsKey(Events.DTSTART) ||modValues.containsKey(Events.STATUS)) {// If this is a cancellation knock it out// of the instances tableif (modValues.containsKey(Events.STATUS) &&modValues.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED) {String[] args = new String[] {String.valueOf(id)};mDb.delete(Tables.INSTANCES, SQL_WHERE_EVENT_ID, args);}// The start time or status of the event changed, so run the// event alarm scheduler.if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "updateInternal() changing event");}mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);}sendUpdateNotification(id, callerIsSyncAdapter);}} else {deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);sendUpdateNotification(callerIsSyncAdapter);}}return cursor.getCount();

1.首先是

 if (modValues.containsKey(Events.DTSTART) ||modValues.containsKey(Events.DTEND) ||modValues.containsKey(Events.DURATION) ||modValues.containsKey(Events.EVENT_TIMEZONE) ||modValues.containsKey(Events.RRULE) ||modValues.containsKey(Events.RDATE) ||modValues.containsKey(Events.EXRULE) ||modValues.containsKey(Events.EXDATE)) {long newLastDate;try {newLastDate = calculateLastDate(values);} catch (DateException de) {throw new IllegalArgumentException("Unable to compute LAST_DATE", de);}

这段的意思,是更新时候会根据这些重复规则去计算最新的最后的日期,然后进行更新。所以我们在设置的时候,其实并不用设置结束时间,会自动计算好

接下去看:

 if (isUpdate) {
.....mInstancesHelper.updateInstancesLocked(modValues, id,false /* not a new event */, mDb);
}else{}

排出其他无用的信息我们会发现,在判断更新后,会去调用InstanceHelper的 update方法,而其实看源码会发现,InstanceHelper,就是对instance的db操作的一个封装,也就是说在这边就会触发instance的更新

直接进入这个方法,然后排除无用的信息

if (!newEvent) {// Want to do this for regular event, recurrence, or exception.// For recurrence or exception, more deletion may happen below if we// do an instance expansion. This deletion will suffice if the// exception// is moved outside the window, for instance.db.delete(Tables.INSTANCES, Instances.EVENT_ID + "=?", new String[] {String.valueOf(rowId)});}。。。。
if (CalendarProvider2.isRecurrenceEvent(rrule, rdate, originalId, originalSyncId)) {
........updateRecurrenceInstancesLocked(values, rowId, db);}

这两行代码就是说,如果当前更新的不是最新事件,那就统统让老的事件见鬼去吧!!!!爸爸不要你们了,重新创建一些列事件,因为我们这次看的是重复Event的更新过程 所以直接定位到updateRecurrenceInstancesLocked

  Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);try {performInstanceExpansion(fields.minInstance, fields.maxInstance,instancesTimezone, entries);} finally {if (entries != null) {entries.close();}}

看这个方法,一开始就是六亲不忍的一顿删除Instance,然后最后再添加上去也就是 performInstanceExpansion ,当初是你说爱我的人是你,现在不要我的也是你

 

到这边为止;我们已经大概知道了一个大概流程,就是一开始进来先去更新最新的EdDate -->然后-->判断Event是否有跟新-->

如果有的话-->删除旧的事件和其相应的Instance --->然后重新创建

 

接下去就要看下规则的制定,也就是怎么把ExDate包含的事件给排除掉,我们先定位到上面的performInstanceExpansion里面去

老规矩抽取关键代码

    RecurrenceSet recur = null;try {recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);} catch (EventRecurrence.InvalidFormatException e) {if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {Log.w(CalendarProvider2.TAG, "Could not parse RRULE recurrence string: "+ rruleStr, e);}continue;}

 

首先是创建一个RecurrenceSet这个类我们后面说。

然后就是

       long[] dates;dates = rp.expand(eventTime, recur, begin, end);

 

根据 RecurrenceSet 计算出时间数组

 

    for (long date : dates) {initialValues = new ContentValues();initialValues.put(Instances.EVENT_ID, eventId);initialValues.put(Instances.BEGIN, date);long dtendMillis = date + durationMillis;initialValues.put(Instances.END, dtendMillis);CalendarInstancesHelperputeTimezoneDependentFields(date, dtendMillis,eventTime, initialValues);instancesMap.add(syncIdKey, initialValues);}

这边大概就是根据规则算出排除exDate的事件列表,然后就行插入,其他代码我也不看,短时间我也看不懂 哈哈哈

 

然后,最后就是RecurrenceSet和RecurrenceProcessor.java是怎么配合算出这些日期数组的

1.首先,RecurrenceSet根据value算出exDates数组,然后分析出rrule的规则

2.RecurrenceProcessor.java 先生根据RecurrenceSet的内容,计算出插入的日期数组

3.然后删除 exDate将我们要删除的date删除

具体代码就是一个解析和删除过程,比较简单,就不分析了,参考如下:

/+/06b3293d5af3454a39681cfd659271551354b8a0/src/com/android/calendarcommon2/RecurrenceProcessor.java

/+/06b3293d5af3454a39681cfd659271551354b8a0/src/com/android/calendarcommon2/RecurrenceSet.java

 

最后还剩一个问题:

为什么日期要设置UTC事件,因为时间的计算代码太多了,多到恶心,暂时也不知道,谁看完有思路的跟我说下哈

好今天的装逼之旅就到这边结束了