Skip to content

Commit 5032503

Browse files
feat: google calendar sync
1 parent a8ddde8 commit 5032503

File tree

1 file changed

+197
-92
lines changed

1 file changed

+197
-92
lines changed

core/src/main/java/org/openedx/core/system/CalendarManager.kt

Lines changed: 197 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class CalendarManager(
2525
) {
2626
private val logger = Logger(TAG)
2727

28+
private data class CalendarAccount(val name: String, val type: String)
29+
2830
val permissions = arrayOf(
2931
android.Manifest.permission.WRITE_CALENDAR,
3032
android.Manifest.permission.READ_CALENDAR
@@ -33,16 +35,10 @@ class CalendarManager(
3335
val accountName: String
3436
get() = getUserAccountForSync()
3537

36-
/**
37-
* Check if the app has the calendar READ/WRITE permissions or not
38-
*/
3938
fun hasPermissions(): Boolean = permissions.all {
4039
PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, it)
4140
}
4241

43-
/**
44-
* Check if the calendar is already existed in mobile calendar app or not
45-
*/
4642
fun isCalendarExist(calendarId: Long): Boolean {
4743
val projection = arrayOf(CalendarContract.Calendars._ID)
4844
val selection = "${CalendarContract.Calendars._ID} = ?"
@@ -62,9 +58,6 @@ class CalendarManager(
6258
return exists
6359
}
6460

65-
/**
66-
* Create or update the calendar if it is already existed in mobile calendar app
67-
*/
6861
fun createOrUpdateCalendar(
6962
calendarId: Long = CALENDAR_DOES_NOT_EXIST,
7063
calendarTitle: String,
@@ -80,22 +73,21 @@ class CalendarManager(
8073
)
8174
}
8275

83-
/**
84-
* Method to create a separate calendar based on course name in mobile calendar app
85-
*/
8676
private fun createCalendar(
8777
calendarTitle: String,
8878
calendarColor: Long
8979
): Long {
80+
val existingGoogleCalendar = findOrCreateGoogleCalendar()
81+
if (existingGoogleCalendar != CALENDAR_DOES_NOT_EXIST) {
82+
return existingGoogleCalendar
83+
}
84+
val calendarAccount = getCalendarOwnerAccount()
9085
val contentValues = ContentValues()
9186
contentValues.put(CalendarContract.Calendars.NAME, calendarTitle)
9287
contentValues.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, calendarTitle)
93-
contentValues.put(CalendarContract.Calendars.ACCOUNT_NAME, accountName)
94-
contentValues.put(
95-
CalendarContract.Calendars.ACCOUNT_TYPE,
96-
CalendarContract.ACCOUNT_TYPE_LOCAL
97-
)
98-
contentValues.put(CalendarContract.Calendars.OWNER_ACCOUNT, accountName)
88+
contentValues.put(CalendarContract.Calendars.ACCOUNT_NAME, calendarAccount.name)
89+
contentValues.put(CalendarContract.Calendars.ACCOUNT_TYPE, calendarAccount.type)
90+
contentValues.put(CalendarContract.Calendars.OWNER_ACCOUNT, calendarAccount.name)
9991
contentValues.put(
10092
CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL,
10193
CalendarContract.Calendars.CAL_ACCESS_ROOT
@@ -106,35 +98,96 @@ class CalendarManager(
10698
CalendarContract.Calendars.CALENDAR_COLOR,
10799
calendarColor.toInt()
108100
)
109-
val creationUri: Uri? = asSyncAdapter(
110-
Uri.parse(CalendarContract.Calendars.CONTENT_URI.toString()),
111-
accountName
101+
102+
val calendarData: Uri? = context.contentResolver.insert(
103+
CalendarContract.Calendars.CONTENT_URI,
104+
contentValues
112105
)
113-
creationUri?.let {
114-
val calendarData: Uri? = context.contentResolver.insert(creationUri, contentValues)
115-
calendarData?.let {
116-
val id = calendarData.lastPathSegment?.toLong()
117-
logger.d { "Calendar ID $id" }
118-
return id ?: CALENDAR_DOES_NOT_EXIST
119-
}
106+
calendarData?.let {
107+
val id = it.lastPathSegment?.toLong()
108+
logger.d { "Calendar ID $id created" }
109+
return id ?: CALENDAR_DOES_NOT_EXIST
110+
}
111+
return CALENDAR_DOES_NOT_EXIST
112+
}
113+
114+
private fun findOrCreateGoogleCalendar(): Long {
115+
findPrimaryGoogleCalendar()?.let {
116+
logger.d { "Using existing primary Google Calendar ID $it" }
117+
return it
118+
}
119+
120+
findWritableGoogleCalendar()?.let {
121+
logger.d { "Using existing Google Calendar ID $it" }
122+
return it
120123
}
124+
125+
logger.d { "No Google Calendar found, will create local calendar" }
121126
return CALENDAR_DOES_NOT_EXIST
122127
}
123128

124-
/**
125-
* Method to add important dates of course as calendar event into calendar of mobile app
126-
*/
129+
private fun findPrimaryGoogleCalendar(): Long? {
130+
val projection = arrayOf(CalendarContract.Calendars._ID)
131+
val selection = "${CalendarContract.Calendars.ACCOUNT_TYPE} = ? AND " +
132+
"${CalendarContract.Calendars.IS_PRIMARY} = 1 AND " +
133+
"${CalendarContract.Calendars.SYNC_EVENTS} = 1 AND " +
134+
"${CalendarContract.Calendars.VISIBLE} = 1"
135+
val selectionArgs = arrayOf(GOOGLE_ACCOUNT_TYPE)
136+
137+
val cursor = context.contentResolver.query(
138+
CalendarContract.Calendars.CONTENT_URI,
139+
projection,
140+
selection,
141+
selectionArgs,
142+
null
143+
)
144+
145+
return cursor?.use {
146+
if (it.moveToFirst()) {
147+
it.getLong(it.getColumnIndexOrThrow(CalendarContract.Calendars._ID))
148+
} else {
149+
null
150+
}
151+
}
152+
}
153+
154+
private fun findWritableGoogleCalendar(): Long? {
155+
val projection = arrayOf(CalendarContract.Calendars._ID)
156+
val selection = "${CalendarContract.Calendars.ACCOUNT_TYPE} = ? AND " +
157+
"${CalendarContract.Calendars.SYNC_EVENTS} = 1 AND " +
158+
"${CalendarContract.Calendars.VISIBLE} = 1 AND " +
159+
"${CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL} >= ?"
160+
val selectionArgs = arrayOf(
161+
GOOGLE_ACCOUNT_TYPE,
162+
CalendarContract.Calendars.CAL_ACCESS_CONTRIBUTOR.toString()
163+
)
164+
165+
val cursor = context.contentResolver.query(
166+
CalendarContract.Calendars.CONTENT_URI,
167+
projection,
168+
selection,
169+
selectionArgs,
170+
"${CalendarContract.Calendars.IS_PRIMARY} DESC"
171+
)
172+
173+
return cursor?.use {
174+
if (it.moveToFirst()) {
175+
it.getLong(it.getColumnIndexOrThrow(CalendarContract.Calendars._ID))
176+
} else {
177+
null
178+
}
179+
}
180+
}
181+
127182
fun addEventsIntoCalendar(
128183
calendarId: Long,
129184
courseId: String,
130185
courseName: String,
131186
courseDateBlock: CourseDateBlock
132187
): Long {
133188
val date = courseDateBlock.date.toCalendar()
134-
// start time of the event, adjusted 1 hour earlier for a 1-hour duration
135-
val startMillis: Long = date.timeInMillis - TimeUnit.HOURS.toMillis(1)
136-
// end time of the event added to the calendar
137-
val endMillis: Long = date.timeInMillis
189+
val startMillis = date.timeInMillis - TimeUnit.HOURS.toMillis(1)
190+
val endMillis = date.timeInMillis
138191

139192
val values = ContentValues().apply {
140193
put(CalendarContract.Events.DTSTART, startMillis)
@@ -160,11 +213,6 @@ class CalendarManager(
160213
return eventId
161214
}
162215

163-
/**
164-
* Method to generate & add deeplink into event description
165-
*
166-
* @return event description with deeplink for assignment block else block title
167-
*/
168216
private fun getEventDescription(
169217
courseId: String,
170218
courseDateBlock: CourseDateBlock,
@@ -194,69 +242,124 @@ class CalendarManager(
194242
return eventDescription
195243
}
196244

197-
/**
198-
* Method to add a reminder to the given calendar events
199-
*
200-
* @param uri Calendar event Uri
201-
*/
202245
private fun addReminderToEvent(uri: Uri) {
203-
val eventId: Long? = uri.lastPathSegment?.toLong()
246+
val eventId = uri.lastPathSegment?.toLong() ?: return
204247
logger.d { "Event ID $eventId" }
205248

206-
// Adding reminder on the start of event
207249
val eventValues = ContentValues().apply {
208-
put(CalendarContract.Reminders.MINUTES, 0)
209250
put(CalendarContract.Reminders.EVENT_ID, eventId)
210251
put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
211252
}
212-
context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, eventValues)
213-
// Adding reminder 24 hours before the event get started
214-
eventValues.apply {
215-
put(CalendarContract.Reminders.MINUTES, TimeUnit.DAYS.toMinutes(1))
216-
}
217-
context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, eventValues)
218-
// Adding reminder 48 hours before the event get started
219-
eventValues.apply {
220-
put(CalendarContract.Reminders.MINUTES, TimeUnit.DAYS.toMinutes(2))
253+
254+
listOf(0, TimeUnit.DAYS.toMinutes(1), TimeUnit.DAYS.toMinutes(2)).forEach { minutes ->
255+
eventValues.put(CalendarContract.Reminders.MINUTES, minutes)
256+
context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, eventValues)
221257
}
222-
context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, eventValues)
223258
}
224259

225-
/**
226-
* Method to delete the course calendar from the mobile calendar app
227-
*/
228260
fun deleteCalendar(calendarId: Long) {
229-
context.contentResolver.delete(
230-
Uri.parse("content://com.android.calendar/calendars/$calendarId"),
231-
null,
261+
deleteAllEventsInCalendar(calendarId)
262+
263+
val calendarAccount = getCalendarAccountById(calendarId)
264+
if (calendarAccount?.type == GOOGLE_ACCOUNT_TYPE) {
265+
logger.d { "Cannot delete Google Calendar, only events were removed" }
266+
return
267+
}
268+
269+
val calendarUri = ContentUris.withAppendedId(
270+
CalendarContract.Calendars.CONTENT_URI,
271+
calendarId
272+
)
273+
val rowsDeleted = context.contentResolver.delete(calendarUri, null, null)
274+
logger.d {
275+
if (rowsDeleted > 0) "Calendar $calendarId deleted successfully"
276+
else "Calendar $calendarId deletion failed or calendar doesn't exist"
277+
}
278+
}
279+
280+
private fun deleteAllEventsInCalendar(calendarId: Long) {
281+
val selection = "${CalendarContract.Events.CALENDAR_ID} = ?"
282+
val selectionArgs = arrayOf(calendarId.toString())
283+
284+
val rowsDeleted = context.contentResolver.delete(
285+
CalendarContract.Events.CONTENT_URI,
286+
selection,
287+
selectionArgs
288+
)
289+
logger.d { "Deleted $rowsDeleted events from calendar $calendarId" }
290+
}
291+
292+
private fun getCalendarOwnerAccount(): CalendarAccount {
293+
getSyncedAccountByType(GOOGLE_ACCOUNT_TYPE)?.let { return it }
294+
getFirstSyncedAccount(excludeLocal = true)?.let { return it }
295+
296+
return CalendarAccount(accountName, CalendarContract.ACCOUNT_TYPE_LOCAL)
297+
}
298+
299+
private fun getFirstSyncedAccount(excludeLocal: Boolean): CalendarAccount? {
300+
val selection = buildString {
301+
append("${CalendarContract.Calendars.SYNC_EVENTS} = 1 AND ${CalendarContract.Calendars.VISIBLE} = 1")
302+
if (excludeLocal) {
303+
append(" AND ${CalendarContract.Calendars.ACCOUNT_TYPE} != ?")
304+
}
305+
}
306+
val selectionArgs = if (excludeLocal) {
307+
arrayOf(CalendarContract.ACCOUNT_TYPE_LOCAL)
308+
} else {
232309
null
310+
}
311+
312+
return queryCalendarAccount(selection, selectionArgs)
313+
}
314+
315+
private fun getSyncedAccountByType(accountType: String): CalendarAccount? {
316+
val selection =
317+
"${CalendarContract.Calendars.ACCOUNT_TYPE} = ? AND ${CalendarContract.Calendars.SYNC_EVENTS} = 1 AND ${CalendarContract.Calendars.VISIBLE} = 1"
318+
val selectionArgs = arrayOf(accountType)
319+
320+
return queryCalendarAccount(selection, selectionArgs)
321+
}
322+
323+
private fun queryCalendarAccount(
324+
selection: String,
325+
selectionArgs: Array<String>?
326+
): CalendarAccount? {
327+
val projection = arrayOf(
328+
CalendarContract.Calendars.ACCOUNT_NAME,
329+
CalendarContract.Calendars.ACCOUNT_TYPE,
330+
CalendarContract.Calendars.IS_PRIMARY
331+
)
332+
val sortOrder = "${CalendarContract.Calendars.IS_PRIMARY} DESC"
333+
334+
val cursor = context.contentResolver.query(
335+
CalendarContract.Calendars.CONTENT_URI,
336+
projection,
337+
selection,
338+
selectionArgs,
339+
sortOrder
233340
)
341+
342+
return cursor?.use {
343+
if (it.moveToFirst()) {
344+
val accountName = it.getString(
345+
it.getColumnIndexOrThrow(CalendarContract.Calendars.ACCOUNT_NAME)
346+
)
347+
val accountType = it.getString(
348+
it.getColumnIndexOrThrow(CalendarContract.Calendars.ACCOUNT_TYPE)
349+
)
350+
CalendarAccount(accountName, accountType)
351+
} else {
352+
null
353+
}
354+
}
234355
}
235356

236-
/**
237-
* Helper method used to return a URI for use with a sync adapter (how an application and a
238-
* sync adapter access the Calendar Provider)
239-
*
240-
* @param uri URI to access the calendar
241-
* @param account Name of the calendar owner
242-
*
243-
* @return URI of the calendar
244-
*
245-
*/
246-
private fun asSyncAdapter(uri: Uri, account: String): Uri? {
247-
return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
248-
.appendQueryParameter(CalendarContract.SyncState.ACCOUNT_NAME, account)
249-
.appendQueryParameter(
250-
CalendarContract.SyncState.ACCOUNT_TYPE,
251-
CalendarContract.ACCOUNT_TYPE_LOCAL
252-
).build()
357+
private fun getCalendarAccountById(calendarId: Long): CalendarAccount? {
358+
val selection = "${CalendarContract.Calendars._ID} = ?"
359+
val selectionArgs = arrayOf(calendarId.toString())
360+
return queryCalendarAccount(selection, selectionArgs)
253361
}
254362

255-
/**
256-
* Method to get the current user account as the Calendar owner
257-
*
258-
* @return calendar owner account or "local_user"
259-
*/
260363
private fun getUserAccountForSync(): String {
261364
return corePreferences.user?.email ?: LOCAL_USER
262365
}
@@ -279,8 +382,10 @@ class CalendarManager(
279382

280383
return cursor?.use {
281384
if (it.moveToFirst()) {
282-
val title = it.getString(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME))
283-
val color = it.getInt(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_COLOR))
385+
val title =
386+
it.getString(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME))
387+
val color =
388+
it.getInt(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_COLOR))
284389
CalendarData(
285390
title = title,
286391
color = color
@@ -294,10 +399,9 @@ class CalendarManager(
294399
fun deleteEvent(eventId: Long) {
295400
val deleteUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId)
296401
val rows = context.contentResolver.delete(deleteUri, null, null)
297-
if (rows > 0) {
298-
logger.d { "Event deleted successfully" }
299-
} else {
300-
logger.d { "Event deletion failed" }
402+
logger.d {
403+
if (rows > 0) "Event deleted successfully"
404+
else "Event deletion failed"
301405
}
302406
}
303407

@@ -306,5 +410,6 @@ class CalendarManager(
306410
const val EVENT_DOES_NOT_EXIST = -1L
307411
private const val TAG = "CalendarManager"
308412
private const val LOCAL_USER = "local_user"
413+
private const val GOOGLE_ACCOUNT_TYPE = "com.google"
309414
}
310415
}

0 commit comments

Comments
 (0)