@@ -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