Skip to content

Commit 2f1d938

Browse files
fix: deleting not courses events
1 parent 2c939c4 commit 2f1d938

File tree

7 files changed

+230
-58
lines changed

7 files changed

+230
-58
lines changed

core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class CalendarInteractor(
2222
return repository.getCourseCalendarEventsByIdFromCache(courseId)
2323
}
2424

25+
suspend fun getAllCourseCalendarEventsFromCache(): List<CourseCalendarEvent> {
26+
return repository.getAllCourseCalendarEventsFromCache()
27+
}
28+
2529
suspend fun deleteCourseCalendarEntitiesByIdFromCache(courseId: String) {
2630
repository.deleteCourseCalendarEntitiesByIdFromCache(courseId)
2731
}

core/src/main/java/org/openedx/core/module/db/CalendarDao.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ interface CalendarDao {
2121
@Query("SELECT * FROM course_calendar_event_table WHERE course_id=:courseId")
2222
suspend fun readCourseCalendarEventsById(courseId: String): List<CourseCalendarEventEntity>
2323

24+
@Query("SELECT * FROM course_calendar_event_table")
25+
suspend fun readAllCourseCalendarEvents(): List<CourseCalendarEventEntity>
26+
2427
@Query("DELETE FROM course_calendar_event_table")
2528
suspend fun clearCourseCalendarEventsCachedData()
2629

core/src/main/java/org/openedx/core/repository/CalendarRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class CalendarRepository(
3030
return calendarDao.readCourseCalendarEventsById(courseId).map { it.mapToDomain() }
3131
}
3232

33+
suspend fun getAllCourseCalendarEventsFromCache(): List<CourseCalendarEvent> {
34+
return calendarDao.readAllCourseCalendarEvents().map { it.mapToDomain() }
35+
}
36+
3337
suspend fun deleteCourseCalendarEntitiesByIdFromCache(courseId: String) {
3438
calendarDao.deleteCourseCalendarEntitiesById(courseId)
3539
}

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

Lines changed: 131 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import androidx.core.content.ContextCompat
1111
import io.branch.indexing.BranchUniversalObject
1212
import io.branch.referral.util.ContentMetadata
1313
import io.branch.referral.util.LinkProperties
14+
import kotlinx.coroutines.delay
15+
import kotlinx.coroutines.runBlocking
1416
import org.openedx.core.data.storage.CorePreferences
1517
import org.openedx.core.domain.model.CalendarData
1618
import org.openedx.core.domain.model.CourseDateBlock
@@ -181,31 +183,60 @@ class CalendarManager(
181183
courseName: String,
182184
courseDateBlock: CourseDateBlock
183185
): Long {
184-
val date = courseDateBlock.date.toCalendar()
185-
val startMillis = date.timeInMillis - TimeUnit.HOURS.toMillis(1)
186-
val endMillis = date.timeInMillis
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+
}
233+
}
234+
}
187235

188-
val values = ContentValues().apply {
189-
put(CalendarContract.Events.DTSTART, startMillis)
190-
put(CalendarContract.Events.DTEND, endMillis)
191-
put(
192-
CalendarContract.Events.TITLE,
193-
"${courseDateBlock.title} : $courseName"
194-
)
195-
put(
196-
CalendarContract.Events.DESCRIPTION,
197-
getEventDescription(
198-
courseId = courseId,
199-
courseDateBlock = courseDateBlock,
200-
isDeeplinkEnabled = corePreferences.appConfig.courseDatesCalendarSync.isDeepLinkEnabled
201-
)
202-
)
203-
put(CalendarContract.Events.CALENDAR_ID, calendarId)
204-
put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
236+
if (eventId == EVENT_DOES_NOT_EXIST) {
237+
logger.d { "Failed to create event after $EVENT_ATTEMPTS attempts" }
205238
}
206-
val uri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values)
207-
uri?.let { addReminderToEvent(uri = it) }
208-
val eventId = uri?.lastPathSegment?.toLong() ?: EVENT_DOES_NOT_EXIST
239+
209240
return eventId
210241
}
211242

@@ -254,11 +285,9 @@ class CalendarManager(
254285
}
255286

256287
fun deleteCalendar(calendarId: Long) {
257-
deleteAllEventsInCalendar(calendarId)
258-
259288
val calendarAccount = getCalendarAccountById(calendarId)
260289
if (calendarAccount?.type == GOOGLE_ACCOUNT_TYPE) {
261-
logger.d { "Cannot delete Google Calendar, only events were removed" }
290+
logger.d { "Cannot delete Google Calendar" }
262291
return
263292
}
264293

@@ -276,16 +305,60 @@ class CalendarManager(
276305
}
277306
}
278307

279-
private fun deleteAllEventsInCalendar(calendarId: Long) {
280-
val selection = "${CalendarContract.Events.CALENDAR_ID} = ?"
281-
val selectionArgs = arrayOf(calendarId.toString())
308+
suspend fun deleteEvents(eventIds: List<Long>) {
309+
val deletedCount = eventIds.count { eventId ->
310+
var deleted = false
311+
var attempts = 0
312+
313+
while (!deleted && attempts < EVENT_ATTEMPTS) {
314+
attempts++
315+
try {
316+
deleted = deleteEventWithRetry(eventId)
317+
if (!deleted && attempts < EVENT_ATTEMPTS) {
318+
delay(ACTION_RETRY_DELAY)
319+
}
320+
} catch (e: Exception) {
321+
logger.d { "Failed to delete event $eventId on attempt $attempts: ${e.message}" }
322+
if (attempts < EVENT_ATTEMPTS) {
323+
delay(ACTION_RETRY_DELAY)
324+
}
325+
}
326+
}
282327

283-
val rowsDeleted = context.contentResolver.delete(
284-
CalendarContract.Events.CONTENT_URI,
285-
selection,
286-
selectionArgs
287-
)
288-
logger.d { "Deleted $rowsDeleted events from calendar $calendarId" }
328+
if (!deleted) {
329+
logger.d { "Failed to delete event $eventId after $EVENT_ATTEMPTS attempts" }
330+
}
331+
332+
deleted
333+
}
334+
logger.d { "Successfully deleted $deletedCount out of ${eventIds.size} events" }
335+
}
336+
337+
private fun deleteEventWithRetry(eventId: Long): Boolean {
338+
val deleteUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId)
339+
val rows = context.contentResolver.delete(deleteUri, null, null)
340+
341+
if (rows > 0) {
342+
// Verify event is actually deleted
343+
val stillExists = isEventExists(eventId)
344+
if (!stillExists) {
345+
logger.d { "Event $eventId deleted successfully" }
346+
return true
347+
} else {
348+
logger.d { "Event $eventId deletion reported success but event still exists" }
349+
return false
350+
}
351+
} else {
352+
// Check if event doesn't exist (might have been already deleted)
353+
val exists = isEventExists(eventId)
354+
if (!exists) {
355+
logger.d { "Event $eventId doesn't exist (already deleted)" }
356+
return true
357+
} else {
358+
logger.d { "Event $eventId deletion failed" }
359+
return false
360+
}
361+
}
289362
}
290363

291364
private fun getCalendarOwnerAccount(): CalendarAccount {
@@ -408,11 +481,34 @@ class CalendarManager(
408481
}
409482
}
410483

484+
private fun isEventExists(eventId: Long): Boolean {
485+
if (eventId == EVENT_DOES_NOT_EXIST) return false
486+
487+
val projection = arrayOf(CalendarContract.Events._ID)
488+
val selection = "${CalendarContract.Events._ID} = ?"
489+
val selectionArgs = arrayOf(eventId.toString())
490+
491+
val cursor = context.contentResolver.query(
492+
CalendarContract.Events.CONTENT_URI,
493+
projection,
494+
selection,
495+
selectionArgs,
496+
null
497+
)
498+
499+
return cursor?.use {
500+
it.count > 0
501+
} ?: false
502+
}
503+
411504
companion object {
412505
const val CALENDAR_DOES_NOT_EXIST = -1L
413506
const val EVENT_DOES_NOT_EXIST = -1L
414507
private const val TAG = "CalendarManager"
415508
private const val LOCAL_USER = "local_user"
416509
private const val GOOGLE_ACCOUNT_TYPE = "com.google"
510+
511+
private const val ACTION_RETRY_DELAY = 500L
512+
private const val EVENT_ATTEMPTS = 3
417513
}
418514
}

profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import androidx.compose.foundation.layout.size
1818
import androidx.compose.foundation.rememberScrollState
1919
import androidx.compose.foundation.shape.CircleShape
2020
import androidx.compose.foundation.verticalScroll
21+
import androidx.compose.material.CircularProgressIndicator
2122
import androidx.compose.material.MaterialTheme
2223
import androidx.compose.material.Text
2324
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.LaunchedEffect
26+
import androidx.compose.runtime.collectAsState
27+
import androidx.compose.runtime.getValue
2428
import androidx.compose.ui.Alignment
2529
import androidx.compose.ui.Modifier
2630
import androidx.compose.ui.draw.clip
@@ -55,18 +59,28 @@ class DisableCalendarSyncDialogFragment : DialogFragment() {
5559
savedInstanceState: Bundle?,
5660
) = ComposeView(requireContext()).apply {
5761
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
62+
dialog?.setCancelable(false)
63+
dialog?.setCanceledOnTouchOutside(false)
5864
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
5965
setContent {
6066
OpenEdXTheme {
6167
val viewModel: DisableCalendarSyncDialogViewModel = koinViewModel()
68+
val isDeleting by viewModel.deletionState.collectAsState()
69+
70+
LaunchedEffect(isDeleting) {
71+
if (isDeleting == DeletionState.DELETED) {
72+
dismiss()
73+
}
74+
}
75+
6276
DisableCalendarSyncDialogView(
6377
calendarData = requireArguments().parcelable<CalendarData>(ARG_CALENDAR_DATA),
78+
isDeleting = isDeleting == DeletionState.DELETING,
6479
onCancelClick = {
6580
dismiss()
6681
},
6782
onDisableSyncingClick = {
6883
viewModel.disableSyncingClick()
69-
dismiss()
7084
}
7185
)
7286
}
@@ -93,13 +107,18 @@ class DisableCalendarSyncDialogFragment : DialogFragment() {
93107
private fun DisableCalendarSyncDialogView(
94108
modifier: Modifier = Modifier,
95109
calendarData: CalendarData?,
110+
isDeleting: Boolean,
96111
onCancelClick: () -> Unit,
97112
onDisableSyncingClick: () -> Unit
98113
) {
99114
val scrollState = rememberScrollState()
100115
DefaultDialogBox(
101116
modifier = modifier,
102-
onDismissClick = onCancelClick
117+
onDismissClick = {
118+
if (!isDeleting) {
119+
onCancelClick()
120+
}
121+
}
103122
) {
104123
Column(
105124
modifier = Modifier
@@ -159,23 +178,42 @@ private fun DisableCalendarSyncDialogView(
159178
style = MaterialTheme.appTypography.bodyMedium,
160179
color = MaterialTheme.appColors.textDark
161180
)
162-
OpenEdXOutlinedButton(
163-
modifier = Modifier.fillMaxWidth(),
164-
text = stringResource(id = R.string.profile_disable_syncing),
165-
backgroundColor = MaterialTheme.appColors.background,
166-
borderColor = MaterialTheme.appColors.primaryButtonBackground,
167-
textColor = MaterialTheme.appColors.primaryButtonBackground,
168-
onClick = {
169-
onDisableSyncingClick()
170-
}
171-
)
172-
OpenEdXButton(
173-
modifier = Modifier.fillMaxWidth(),
174-
text = stringResource(id = coreR.string.core_cancel),
175-
onClick = {
176-
onCancelClick()
181+
182+
if (isDeleting) {
183+
Column(
184+
modifier = Modifier.fillMaxWidth(),
185+
horizontalAlignment = Alignment.CenterHorizontally,
186+
verticalArrangement = Arrangement.spacedBy(12.dp)
187+
) {
188+
CircularProgressIndicator(
189+
modifier = Modifier.size(24.dp),
190+
color = MaterialTheme.appColors.primary
191+
)
192+
Text(
193+
text = stringResource(id = R.string.profile_deleting_events),
194+
style = MaterialTheme.appTypography.bodyMedium,
195+
color = MaterialTheme.appColors.textDark
196+
)
177197
}
178-
)
198+
} else {
199+
OpenEdXOutlinedButton(
200+
modifier = Modifier.fillMaxWidth(),
201+
text = stringResource(id = R.string.profile_disable_syncing),
202+
backgroundColor = MaterialTheme.appColors.background,
203+
borderColor = MaterialTheme.appColors.primaryButtonBackground,
204+
textColor = MaterialTheme.appColors.primaryButtonBackground,
205+
onClick = {
206+
onDisableSyncingClick()
207+
}
208+
)
209+
OpenEdXButton(
210+
modifier = Modifier.fillMaxWidth(),
211+
text = stringResource(id = coreR.string.core_cancel),
212+
onClick = {
213+
onCancelClick()
214+
}
215+
)
216+
}
179217
}
180218
}
181219
}
@@ -187,8 +225,9 @@ private fun DisableCalendarSyncDialogPreview() {
187225
OpenEdXTheme {
188226
DisableCalendarSyncDialogView(
189227
calendarData = CalendarData("calendar", Color.GREEN),
228+
isDeleting = true,
190229
onCancelClick = { },
191-
onDisableSyncingClick = { }
230+
onDisableSyncingClick = { },
192231
)
193232
}
194233
}

0 commit comments

Comments
 (0)