@@ -3,6 +3,7 @@ package org.openedx.core.system
33import android.content.ContentUris
44import android.content.ContentValues
55import android.content.Context
6+ import android.content.Intent
67import android.content.pm.PackageManager
78import android.database.Cursor
89import android.net.Uri
@@ -15,7 +16,9 @@ import kotlinx.coroutines.delay
1516import kotlinx.coroutines.runBlocking
1617import org.openedx.core.data.storage.CorePreferences
1718import org.openedx.core.domain.model.CalendarData
19+ import org.openedx.core.domain.model.CalendarType
1820import org.openedx.core.domain.model.CourseDateBlock
21+ import org.openedx.core.domain.model.UserCalendar
1922import org.openedx.core.utils.Logger
2023import org.openedx.core.utils.toCalendar
2124import java.util.TimeZone
@@ -63,28 +66,36 @@ class CalendarManager(
6366 fun createOrUpdateCalendar (
6467 calendarId : Long = CALENDAR_DOES_NOT_EXIST ,
6568 calendarTitle : String ,
66- calendarColor : Long
69+ calendarColor : Long ,
70+ calendarType : CalendarType
6771 ): Long {
68- if (calendarId != CALENDAR_DOES_NOT_EXIST ) {
72+ if (calendarId != CALENDAR_DOES_NOT_EXIST && calendarType == CalendarType . LOCAL ) {
6973 deleteCalendar(calendarId = calendarId)
7074 }
7175
7276 return createCalendar(
7377 calendarTitle = calendarTitle,
74- calendarColor = calendarColor
78+ calendarColor = calendarColor,
79+ calendarType = calendarType
7580 )
7681 }
7782
7883 private fun createCalendar (
7984 calendarTitle : String ,
80- calendarColor : Long
85+ calendarColor : Long ,
86+ calendarType : CalendarType
8187 ): Long {
82- val existingGoogleCalendar = findOrCreateGoogleCalendar()
83- if (existingGoogleCalendar != CALENDAR_DOES_NOT_EXIST ) {
84- return existingGoogleCalendar
88+ if (calendarType == CalendarType .GOOGLE ) {
89+ val existingGoogleCalendar = findOrCreateGoogleCalendar()
90+ if (existingGoogleCalendar != CALENDAR_DOES_NOT_EXIST ) {
91+ return existingGoogleCalendar
92+ }
8593 }
8694
87- val calendarAccount = getCalendarOwnerAccount()
95+ val calendarAccount = when (calendarType) {
96+ CalendarType .LOCAL -> CalendarAccount (accountName, CalendarContract .ACCOUNT_TYPE_LOCAL )
97+ CalendarType .GOOGLE -> getCalendarOwnerAccount()
98+ }
8899 val contentValues = ContentValues ().apply {
89100 put(CalendarContract .Calendars .NAME , calendarTitle)
90101 put(CalendarContract .Calendars .CALENDAR_DISPLAY_NAME , calendarTitle)
@@ -177,67 +188,128 @@ class CalendarManager(
177188 }
178189 }
179190
191+ fun getGoogleCalendars (): List <UserCalendar > {
192+ val projection = arrayOf(
193+ CalendarContract .Calendars ._ID ,
194+ CalendarContract .Calendars .CALENDAR_DISPLAY_NAME ,
195+ CalendarContract .Calendars .CALENDAR_COLOR
196+ )
197+ val selection = " ${CalendarContract .Calendars .ACCOUNT_TYPE } = ? AND " +
198+ " ${CalendarContract .Calendars .SYNC_EVENTS } = 1 AND " +
199+ " ${CalendarContract .Calendars .VISIBLE } = 1 AND " +
200+ " ${CalendarContract .Calendars .CALENDAR_ACCESS_LEVEL } >= ?"
201+ val selectionArgs = arrayOf(
202+ GOOGLE_ACCOUNT_TYPE ,
203+ CalendarContract .Calendars .CAL_ACCESS_CONTRIBUTOR .toString()
204+ )
205+ val sortOrder =
206+ " ${CalendarContract .Calendars .IS_PRIMARY } DESC, ${CalendarContract .Calendars .CALENDAR_DISPLAY_NAME } ASC"
207+
208+ return try {
209+ val cursor = context.contentResolver.query(
210+ CalendarContract .Calendars .CONTENT_URI ,
211+ projection,
212+ selection,
213+ selectionArgs,
214+ sortOrder
215+ )
216+
217+ cursor?.use {
218+ val idIndex = it.getColumnIndexOrThrow(CalendarContract .Calendars ._ID )
219+ val titleIndex =
220+ it.getColumnIndexOrThrow(CalendarContract .Calendars .CALENDAR_DISPLAY_NAME )
221+ val colorIndex = it.getColumnIndexOrThrow(CalendarContract .Calendars .CALENDAR_COLOR )
222+
223+ buildList {
224+ while (it.moveToNext()) {
225+ add(
226+ UserCalendar (
227+ id = it.getLong(idIndex),
228+ title = it.getString(titleIndex),
229+ color = it.getInt(colorIndex)
230+ )
231+ )
232+ }
233+ }
234+ } ? : emptyList()
235+ } catch (e: SecurityException ) {
236+ logger.d { " Failed to load Google calendars: ${e.message} " }
237+ emptyList()
238+ }
239+ }
240+
241+ fun hasAlternativeCalendarApp (): Boolean {
242+ val intent = Intent (Intent .ACTION_INSERT ).setData(CalendarContract .Events .CONTENT_URI )
243+ val activities = context.packageManager.queryIntentActivities(intent, 0 )
244+ return activities.any { it.activityInfo.packageName != GOOGLE_CALENDAR_PACKAGE }
245+ }
246+
180247 fun addEventsIntoCalendar (
181248 calendarId : Long ,
182249 courseId : String ,
183250 courseName : String ,
184251 courseDateBlock : CourseDateBlock
185252 ): Long {
186- var eventId = EVENT_DOES_NOT_EXIST
187- var attempts = 0
188-
189- while (eventId == EVENT_DOES_NOT_EXIST && attempts < EVENT_ATTEMPTS ) {
190- attempts++
191- try {
192- val date = courseDateBlock.date.toCalendar()
193- val startMillis = date.timeInMillis - TimeUnit .HOURS .toMillis(1 )
194- val endMillis = date.timeInMillis
195-
196- val values = ContentValues ().apply {
197- put(CalendarContract .Events .DTSTART , startMillis)
198- put(CalendarContract .Events .DTEND , endMillis)
199- put(
200- CalendarContract .Events .TITLE ,
201- " ${courseDateBlock.title} : $courseName "
202- )
203- put(
204- CalendarContract .Events .DESCRIPTION ,
205- getEventDescription(
206- courseId = courseId,
207- courseDateBlock = courseDateBlock,
208- isDeeplinkEnabled = corePreferences.appConfig.courseDatesCalendarSync.isDeepLinkEnabled
209- )
210- )
211- put(CalendarContract .Events .CALENDAR_ID , calendarId)
212- put(CalendarContract .Events .EVENT_TIMEZONE , TimeZone .getDefault().id)
213- }
214- val uri =
215- context.contentResolver.insert(CalendarContract .Events .CONTENT_URI , values)
216- val insertedEventId = uri?.lastPathSegment?.toLong() ? : EVENT_DOES_NOT_EXIST
217-
218- if (insertedEventId != EVENT_DOES_NOT_EXIST && isEventExists(insertedEventId)) {
219- uri?.let { addReminderToEvent(uri = it) }
220- eventId = insertedEventId
221- logger.d { " Event created successfully: $eventId (attempt $attempts )" }
222- } else {
223- logger.d { " Event creation failed, retrying... (attempt $attempts /$EVENT_ATTEMPTS )" }
224- if (attempts < EVENT_ATTEMPTS ) {
225- runBlocking { delay(ACTION_RETRY_DELAY ) }
226- }
227- }
228- } catch (e: Exception ) {
229- logger.d { " Event creation error on attempt $attempts : ${e.message} " }
230- if (attempts < EVENT_ATTEMPTS ) {
231- runBlocking { delay(ACTION_RETRY_DELAY ) }
232- }
253+ repeat(EVENT_ATTEMPTS ) { attemptIndex ->
254+ val attemptNumber = attemptIndex + 1
255+ val eventId =
256+ tryCreateEvent(calendarId, courseId, courseName, courseDateBlock, attemptNumber)
257+ if (eventId != EVENT_DOES_NOT_EXIST ) {
258+ return eventId
259+ }
260+ if (attemptNumber < EVENT_ATTEMPTS ) {
261+ runBlocking { delay(ACTION_RETRY_DELAY ) }
233262 }
234263 }
264+ logger.d { " Failed to create event after $EVENT_ATTEMPTS attempts" }
265+ return EVENT_DOES_NOT_EXIST
266+ }
235267
236- if (eventId == EVENT_DOES_NOT_EXIST ) {
237- logger.d { " Failed to create event after $EVENT_ATTEMPTS attempts" }
238- }
268+ private fun tryCreateEvent (
269+ calendarId : Long ,
270+ courseId : String ,
271+ courseName : String ,
272+ courseDateBlock : CourseDateBlock ,
273+ attemptNumber : Int
274+ ): Long {
275+ return try {
276+ val date = courseDateBlock.date.toCalendar()
277+ val startMillis = date.timeInMillis - TimeUnit .HOURS .toMillis(1 )
278+ val endMillis = date.timeInMillis
279+
280+ val values = ContentValues ().apply {
281+ put(CalendarContract .Events .DTSTART , startMillis)
282+ put(CalendarContract .Events .DTEND , endMillis)
283+ put(
284+ CalendarContract .Events .TITLE ,
285+ " ${courseDateBlock.title} : $courseName "
286+ )
287+ put(
288+ CalendarContract .Events .DESCRIPTION ,
289+ getEventDescription(
290+ courseId = courseId,
291+ courseDateBlock = courseDateBlock,
292+ isDeeplinkEnabled = corePreferences.appConfig.courseDatesCalendarSync.isDeepLinkEnabled
293+ )
294+ )
295+ put(CalendarContract .Events .CALENDAR_ID , calendarId)
296+ put(CalendarContract .Events .EVENT_TIMEZONE , TimeZone .getDefault().id)
297+ }
298+ val uri = context.contentResolver.insert(CalendarContract .Events .CONTENT_URI , values)
299+ val insertedEventId = uri?.lastPathSegment?.toLong() ? : EVENT_DOES_NOT_EXIST
239300
240- return eventId
301+ if (insertedEventId != EVENT_DOES_NOT_EXIST && isEventExists(insertedEventId)) {
302+ uri?.let { addReminderToEvent(uri = it) }
303+ logger.d { " Event created successfully: $insertedEventId (attempt $attemptNumber )" }
304+ insertedEventId
305+ } else {
306+ logger.d { " Event creation failed, retrying... (attempt $attemptNumber /$EVENT_ATTEMPTS )" }
307+ EVENT_DOES_NOT_EXIST
308+ }
309+ } catch (e: Exception ) {
310+ logger.d { " Event creation error on attempt $attemptNumber : ${e.message} " }
311+ EVENT_DOES_NOT_EXIST
312+ }
241313 }
242314
243315 private fun getEventDescription (
@@ -338,27 +410,26 @@ class CalendarManager(
338410 val deleteUri = ContentUris .withAppendedId(CalendarContract .Events .CONTENT_URI , eventId)
339411 val rows = context.contentResolver.delete(deleteUri, null , null )
340412
341- if (rows > 0 ) {
342- // Verify event is actually deleted
413+ val deleted = if (rows > 0 ) {
343414 val stillExists = isEventExists(eventId)
344415 if (! stillExists) {
345416 logger.d { " Event $eventId deleted successfully" }
346- return true
417+ true
347418 } else {
348419 logger.d { " Event $eventId deletion reported success but event still exists" }
349- return false
420+ false
350421 }
351422 } else {
352- // Check if event doesn't exist (might have been already deleted)
353423 val exists = isEventExists(eventId)
354424 if (! exists) {
355425 logger.d { " Event $eventId doesn't exist (already deleted)" }
356- return true
426+ true
357427 } else {
358428 logger.d { " Event $eventId deletion failed" }
359- return false
429+ false
360430 }
361431 }
432+ return deleted
362433 }
363434
364435 private fun getCalendarOwnerAccount (): CalendarAccount {
@@ -507,6 +578,7 @@ class CalendarManager(
507578 private const val TAG = " CalendarManager"
508579 private const val LOCAL_USER = " local_user"
509580 private const val GOOGLE_ACCOUNT_TYPE = " com.google"
581+ private const val GOOGLE_CALENDAR_PACKAGE = " com.google.android.calendar"
510582
511583 private const val ACTION_RETRY_DELAY = 500L
512584 private const val EVENT_ATTEMPTS = 3
0 commit comments