Deleting calendars (that are created through this library) not working on Android
When deleting a calendar on Android there is a check on ACCOUNT_NAME and ACCOUNT_TYPE (see). These are not filled in when you create a calendar through this library.
The fix for this is two-fold:
- Provide these two values when creating a calendar from this library so they can be deleted as well
- Add a check for these two fields when deleting and throw a descriptive exception for calendars that are created outside of this library
Unfortunately, this is a very important issue for Android users! If this issue is not resolved, this library is practically useless in a production evironment.
You cannot delete the calendars created by an application using this library (I mean like you cannot delete them at all)! Not even via calendar apps (Samsung Calendar, Google Calendar, basically all calendar apps). The only way to remove these calendars is to delete the data from the calendar store (system application). Even a "patched" version of this library cannot fix this problem afterwards.
For nerds: The current approach creates the calendars with a "null" owner account, which you will not be able to provide/query when deleting the calendar. To delete a calendar on Android, you MUST provide the exact owner account of the calendar you want to delete (which is again null in our case and cannot be provided/queried).
A production tested code I use is following:
public async Task<string> CreateCalendar(string name, Color? color = null)
{
await EnsureWriteCalendarPermission();
ContentValues calendarToCreate = new();
calendarToCreate.Put(CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName, name);
calendarToCreate.Put(CalendarContract.Calendars.InterfaceConsts.CalendarAccessLevel, (int)CalendarAccess.AccessOwner);
calendarToCreate.Put(CalendarContract.Calendars.InterfaceConsts.OwnerAccount, AppInfo.Current.PackageName);
calendarToCreate.Put(CalendarContract.Calendars.InterfaceConsts.AccountName, AppInfo.Current.Name);
calendarToCreate.Put(CalendarContract.Calendars.InterfaceConsts.AccountType, CalendarContract.AccountTypeLocal);
if (color is not null)
{
calendarToCreate.Put(CalendarContract.Calendars.InterfaceConsts.CalendarColor, color.AsColor());
}
try
{
var QueryString = CalendarContract.CallerIsSyncadapter + "=true" + "&" + CalendarContract.Calendars.InterfaceConsts.AccountName + "=" + AppInfo.Current.Name + "&" + CalendarContract.Calendars.InterfaceConsts.AccountType + "=" + CalendarContract.AccountTypeLocal;
// Add CalendarContract.CALLER_IS_SYNCADAPTER, ACCOUNT_NAME and ACCOUNT_TYPE to the Uri as Query-Parameter.
if (calendarsTableUri.ToString().Contains("?"))
{
QueryString = "&" + QueryString;
}
else
{
QueryString = "?" + QueryString;
}
var finalUri = Android.Net.Uri.Parse(calendarsTableUri.ToString() + QueryString);
var idUrl = platformContentResolver?.Insert(finalUri,
calendarToCreate);
if (!long.TryParse(idUrl?.LastPathSegment, out var savedId))
{
throw new CalendarStoreException(
"There was an error saving the calendar.");
}
return savedId.ToString();
}
catch
{
// You míght want to logcat the error here.
return "";
}
}
The DeleteCalendar has to look something like this:
public async Task DeleteCalendar(string calendarId)
{
await EnsureWriteCalendarPermission();
// Android ids are always integers
if (string.IsNullOrEmpty(calendarId) ||
!long.TryParse(calendarId, out long platformCalendarId))
{
throw InvalidCalendar(calendarId);
}
// We just want to know a calendar with this ID exists
_ = await GetPlatformCalendar(calendarId);
var deleteEventUri = ContentUris.WithAppendedId(calendarsTableUri, platformCalendarId);
var QueryString = CalendarContract.CallerIsSyncadapter + "=true" + "&" + CalendarContract.Calendars.InterfaceConsts.AccountName + "=" + AppInfo.Current.Name + "&" + CalendarContract.Calendars.InterfaceConsts.AccountType + "=" + CalendarContract.AccountTypeLocal;
// Add CalendarContract.CALLER_IS_SYNCADAPTER, ACCOUNT_NAME and ACCOUNT_TYPE to the Uri as Query-Parameter.
if (deleteEventUri.ToString().Contains("?"))
{
QueryString = "&" + QueryString;
} else
{
QueryString = "?" + QueryString;
}
// combine uri with query-parameter
var finalDeleteEventUri = Android.Net.Uri.Parse(deleteEventUri.ToString() + QueryString);
Debug.WriteLine("Final Delete-Calendar Uri = " + finalDeleteEventUri.ToString());
var deleteCount = platformContentResolver?.Delete(finalDeleteEventUri, null, null);
if (deleteCount != 1)
{
throw new CalendarStoreException(
"There was an error deleting the calendar.");
}
}
You can adjust the logic behind the different paramters, but this one works out of the box without any breaking changes to the original library! Unfortunately I cannot do a pull-request as I had to heavily modify the code to work with my project, which turned out to merge with my project.
I just ran into the same issue. Based on https://stackoverflow.com/a/38633056, I temporarily added the following to my app to add an account name to the calendar(s) that were missing it:
var calendarsWithoutNameQuery = CalendarContract.Calendars.InterfaceConsts.AccountName + " IS NULL";
var updateUri = CalendarContract.Calendars.ContentUri
?.BuildUpon()
?.AppendQueryParameter(CalendarContract.CallerIsSyncadapter, "true")
?.AppendQueryParameter(CalendarContract.Calendars.InterfaceConsts.AccountName, "MyAccount")
?.AppendQueryParameter(CalendarContract.Calendars.InterfaceConsts.AccountType, CalendarContract.AccountTypeLocal)
?.Build();
ContentValues values = new ContentValues();
values.Put(CalendarContract.Calendars.InterfaceConsts.AccountName, "MyAccount");
values.Put(CalendarContract.Calendars.InterfaceConsts.AccountType, CalendarContract.AccountTypeLocal);
int count = Platform.AppContext.ApplicationContext?.ContentResolver?.Update(updateUri, values, calendarsWithoutNameQuery, null) ?? 0;
This allowed me to delete the calendar again.
To prevent this issue, I think CreateCalendar should be extended to always add an account name and account type (defaulting to CalendarContract.AccountTypeLocal?). According to these docs, calendar creation should be done as a sync adapter.
Always happy to see more PRs if you got more ideas!