Skip to content

Commit 2cbb408

Browse files
fix: switching between local and google calendars. Google calendars options.
1 parent 2f1d938 commit 2cbb408

File tree

11 files changed

+486
-104
lines changed

11 files changed

+486
-104
lines changed

app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.openedx.core.data.storage.CalendarPreferences
88
import org.openedx.core.data.storage.CorePreferences
99
import org.openedx.core.data.storage.InAppReviewPreferences
1010
import org.openedx.core.domain.model.AppConfig
11+
import org.openedx.core.domain.model.CalendarType
1112
import org.openedx.core.domain.model.VideoQuality
1213
import org.openedx.core.domain.model.VideoSettings
1314
import org.openedx.core.system.CalendarManager
@@ -69,6 +70,7 @@ class PreferencesManager(context: Context) :
6970
override fun clearCalendarPreferences() {
7071
sharedPreferences.edit().apply {
7172
remove(CALENDAR_ID)
73+
remove(CALENDAR_TYPE)
7274
remove(IS_CALENDAR_SYNC_ENABLED)
7375
remove(HIDE_INACTIVE_COURSES)
7476
}.apply()
@@ -104,6 +106,17 @@ class PreferencesManager(context: Context) :
104106
}
105107
get() = getLong(CALENDAR_ID, CalendarManager.CALENDAR_DOES_NOT_EXIST)
106108

109+
override var calendarType: CalendarType
110+
set(value) {
111+
saveString(CALENDAR_TYPE, value.name)
112+
}
113+
get() {
114+
val storedType = getString(CALENDAR_TYPE, CalendarType.LOCAL.name)
115+
return runCatching {
116+
CalendarType.valueOf(storedType)
117+
}.getOrDefault(CalendarType.LOCAL)
118+
}
119+
107120
override var user: User?
108121
set(value) {
109122
val userJson = Gson().toJson(value)
@@ -234,6 +247,7 @@ class PreferencesManager(context: Context) :
234247
private const val VIDEO_SETTINGS_DOWNLOAD_QUALITY = "video_settings_download_quality"
235248
private const val APP_CONFIG = "app_config"
236249
private const val CALENDAR_ID = "CALENDAR_ID"
250+
private const val CALENDAR_TYPE = "CALENDAR_TYPE"
237251
private const val RESET_APP_DIRECTORY = "reset_app_directory"
238252
private const val IS_CALENDAR_SYNC_ENABLED = "IS_CALENDAR_SYNC_ENABLED"
239253
private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED"

auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import org.openedx.core.config.MicrosoftConfig
3737
import org.openedx.core.data.storage.CalendarPreferences
3838
import org.openedx.core.data.storage.CorePreferences
3939
import org.openedx.core.domain.interactor.CalendarInteractor
40+
import org.openedx.core.domain.model.CalendarType
4041
import org.openedx.core.presentation.global.WhatsNewGlobalManager
4142
import org.openedx.core.system.EdxError
4243
import org.openedx.core.system.notifier.app.AppNotifier
@@ -91,6 +92,7 @@ class SignInViewModelTest {
9192
every { config.getGoogleConfig() } returns GoogleConfig()
9293
every { config.getMicrosoftConfig() } returns MicrosoftConfig()
9394
every { calendarPreferences.calendarUser } returns ""
95+
every { calendarPreferences.calendarType } returns CalendarType.LOCAL
9496
every { calendarPreferences.clearCalendarPreferences() } returns Unit
9597
coEvery { calendarInteractor.clearCalendarCachedData() } returns Unit
9698
every { analytics.logScreenEvent(any(), any()) } returns Unit

core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.openedx.core.data.storage
22

3+
import org.openedx.core.domain.model.CalendarType
4+
35
interface CalendarPreferences {
46
var calendarId: Long
57
var calendarUser: String
8+
var calendarType: CalendarType
69
var isCalendarSyncEnabled: Boolean
710
var isHideInactiveCourses: Boolean
811

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.openedx.core.domain.model
2+
3+
enum class CalendarType {
4+
LOCAL,
5+
GOOGLE,
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.openedx.core.domain.model
2+
3+
data class UserCalendar(
4+
val id: Long,
5+
val title: String,
6+
val color: Int
7+
)

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

Lines changed: 138 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.openedx.core.system
33
import android.content.ContentUris
44
import android.content.ContentValues
55
import android.content.Context
6+
import android.content.Intent
67
import android.content.pm.PackageManager
78
import android.database.Cursor
89
import android.net.Uri
@@ -15,7 +16,9 @@ import kotlinx.coroutines.delay
1516
import kotlinx.coroutines.runBlocking
1617
import org.openedx.core.data.storage.CorePreferences
1718
import org.openedx.core.domain.model.CalendarData
19+
import org.openedx.core.domain.model.CalendarType
1820
import org.openedx.core.domain.model.CourseDateBlock
21+
import org.openedx.core.domain.model.UserCalendar
1922
import org.openedx.core.utils.Logger
2023
import org.openedx.core.utils.toCalendar
2124
import 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

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,16 @@ class CalendarViewModel(
124124
}
125125

126126
private fun getCalendarData() {
127-
if (calendarManager.hasPermissions()) {
128-
val calendarData = calendarManager.getCalendarData(calendarId = calendarPreferences.calendarId)
129-
_uiState.update { it.copy(calendarData = calendarData) }
127+
if (!calendarManager.hasPermissions()) return
128+
129+
val calendarId = calendarPreferences.calendarId
130+
if (calendarId == CalendarManager.CALENDAR_DOES_NOT_EXIST) {
131+
_uiState.update { it.copy(calendarData = null) }
132+
return
130133
}
134+
135+
val calendarData = calendarManager.getCalendarData(calendarId = calendarId)
136+
_uiState.update { it.copy(calendarData = calendarData) }
131137
}
132138

133139
private fun updateSyncedCoursesCount() {

0 commit comments

Comments
 (0)