From e73a3928f10987f28aa5160eb3e7b8cd10e8a9b3 Mon Sep 17 00:00:00 2001 From: doomsdayrs Date: Sun, 14 Apr 2024 13:40:31 -0400 Subject: [PATCH] Split CalendarRepository into Repository & DataSources scheme CalendarRepository is currently a monolithic file that contains all operations related to Calendars. While it is true that the Repository is meant to be a source of truth. It is not the case to have all operations in it. Instead, splitting the repositorys data accessor functions into datasources lowers the code complexity of the repository itself, letting it focus on proper tasks such as actually handling dataflow and truth. A final adjustment is adding an interface between the actual implementation of the repository and the UI layer. This allows UI developers to not be overwhelmed looking at implementation code, and instead focus on accessing methods they need. Another small change made in this is replacing the usage of LiveData in the data layer. LiveData is a UI level structure meant best to be used with old android views, not for the data layer. Replacement with Kotlin Flow fits the proper scheme for what the data layer is tasked with. To lower the amount of changes, the UI layer handles the change of the Flow into LiveData via the `asLiveData()` extension function. This change also removes ContentProviderLiveData.kt, which is simply replaced with `callbackFlow`. Copyright has also been updated for respective files. --- app/build.gradle.kts | 3 + .../calendar/datasource/AccountDataSource.kt | 64 ++++ .../calendar/datasource/CalendarDataSource.kt | 286 ++++++++++++++++++ .../calendar/datasource/EventDataSource.kt | 53 ++++ .../persistence/CalendarRepository.kt | 226 +++----------- .../persistence/ContentProviderLiveData.kt | 75 ----- .../persistence/ICalendarRepository.kt | 82 +++++ .../calendar/settings/CalendarPreferences.kt | 6 +- .../calendar/settings/MainListViewModel.kt | 7 +- 9 files changed, 540 insertions(+), 262 deletions(-) create mode 100644 app/src/main/java/com/android/calendar/datasource/AccountDataSource.kt create mode 100644 app/src/main/java/com/android/calendar/datasource/CalendarDataSource.kt create mode 100644 app/src/main/java/com/android/calendar/datasource/EventDataSource.kt delete mode 100644 app/src/main/java/com/android/calendar/persistence/ContentProviderLiveData.kt create mode 100644 app/src/main/java/com/android/calendar/persistence/ICalendarRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3d7effa..06df3f2e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,6 +137,9 @@ dependencies { // https://mvnrepository.com/artifact/org.dmfs/lib-recur implementation("org.dmfs:lib-recur:0.16.0") + // lifecycle + val lifecycle_version = "2.7.0" + implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version") } tasks.preBuild.dependsOn(":aarGen") diff --git a/app/src/main/java/com/android/calendar/datasource/AccountDataSource.kt b/app/src/main/java/com/android/calendar/datasource/AccountDataSource.kt new file mode 100644 index 00000000..37d39d00 --- /dev/null +++ b/app/src/main/java/com/android/calendar/datasource/AccountDataSource.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 The Etar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.android.calendar.datasource + +import android.accounts.Account +import android.app.Application +import android.content.ContentResolver +import android.content.ContentUris +import android.provider.CalendarContract + +/** + * Datasource of Account entities + */ +class AccountDataSource( + private val application: Application +) { + /** + * Convenience to get the content resolver + */ + private val contentResolver: ContentResolver + get() = application.contentResolver + + /** + * TODO Document + */ + fun queryAccount(calendarId: Long): Account? { + val calendarUri = + ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId) + contentResolver.query(calendarUri, ACCOUNT_PROJECTION, null, null, null) + ?.use { + if (it.moveToFirst()) { + val accountName = it.getString(PROJECTION_ACCOUNT_INDEX_NAME) + val accountType = it.getString(PROJECTION_ACCOUNT_INDEX_TYPE) + return Account(accountName, accountType) // TODO Is this the right type? + } + } + return null + } + + companion object { + private val ACCOUNT_PROJECTION = arrayOf( + CalendarContract.Calendars.ACCOUNT_NAME, + CalendarContract.Calendars.ACCOUNT_TYPE + ) + + private const val PROJECTION_ACCOUNT_INDEX_NAME = 0 + private const val PROJECTION_ACCOUNT_INDEX_TYPE = 1 + } +} diff --git a/app/src/main/java/com/android/calendar/datasource/CalendarDataSource.kt b/app/src/main/java/com/android/calendar/datasource/CalendarDataSource.kt new file mode 100644 index 00000000..92fc19f1 --- /dev/null +++ b/app/src/main/java/com/android/calendar/datasource/CalendarDataSource.kt @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2024 The Etar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.android.calendar.datasource + +import android.app.Application +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.database.ContentObserver +import android.net.Uri +import android.provider.CalendarContract +import com.android.calendar.Utils +import com.android.calendar.persistence.Calendar +import com.android.calendar.persistence.CalendarRepository +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import ws.xsoh.etar.R + +/** + * Datasource for Calendar entities + */ +class CalendarDataSource( + private val application: Application +) { + /** + * Convenience to get the content resolver + */ + private val contentResolver: ContentResolver + get() = application.contentResolver + + /** + * Get all calendars + */ + private fun getContentProviderValue(): List { + val calendars: MutableList = mutableListOf() + + contentResolver.query( + CalendarContract.Calendars.CONTENT_URI, + PROJECTION, + null, + null, + CalendarContract.Calendars.ACCOUNT_NAME + )?.use { + while (it.moveToNext()) { + val id = it.getLong(PROJECTION_INDEX_ID) + val accountName = it.getString(PROJECTION_INDEX_ACCOUNT_NAME) + val accountType = it.getString(PROJECTION_INDEX_ACCOUNT_TYPE) + val name = it.getString(PROJECTION_INDEX_NAME) + val displayName = + it.getString(PROJECTION_INDEX_CALENDAR_DISPLAY_NAME) + val color = it.getInt(PROJECTION_INDEX_CALENDAR_COLOR) + val visible = it.getInt(PROJECTION_INDEX_VISIBLE) == 1 + val syncEvents = it.getInt(PROJECTION_INDEX_SYNC_EVENTS) == 1 + val isPrimary = it.getInt(PROJECTION_INDEX_IS_PRIMARY) == 1 + val isLocal = accountType == CalendarContract.ACCOUNT_TYPE_LOCAL + + calendars.add( + Calendar( + id = id, + accountName = accountName, + accountType = accountType, + name = name, + displayName = displayName, + color = color, + visible = visible, + syncEvents = syncEvents, + isPrimary = isPrimary, + isLocal = isLocal + ) + ) + } + } + return calendars + } + + /** + * Get a flow of all calendars. + * + * Updates on any changes. + */ + fun getAllCalendars(): Flow> = + callbackFlow { + val observer = object : ContentObserver(null) { + override fun onChange(self: Boolean) { + // Notify collectors that data at the uri has changed + trySend(getContentProviderValue()) + } + } + + if (Utils.isCalendarPermissionGranted(application, true)) { + contentResolver.registerContentObserver( + CalendarContract.Calendars.CONTENT_URI, + true, + observer + ) + trySend(getContentProviderValue()) + } + + awaitClose { + contentResolver.unregisterContentObserver(observer) + } + } + + /** + * Creates the content values needed to insert a local account. + */ + private fun buildLocalCalendarContentValues( + accountName: String, + displayName: String + ): ContentValues { + val internalName = "etar_local_" + displayName.replace("[^a-zA-Z0-9]".toRegex(), "") + + return ContentValues().apply { + put(CalendarContract.Calendars.ACCOUNT_NAME, accountName) + put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) + put(CalendarContract.Calendars.OWNER_ACCOUNT, accountName) + put(CalendarContract.Calendars.NAME, internalName) + put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName) + put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, DEFAULT_COLOR_KEY) + put( + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, + CalendarContract.Calendars.CAL_ACCESS_ROOT + ) + put(CalendarContract.Calendars.VISIBLE, 1) + put(CalendarContract.Calendars.SYNC_EVENTS, 1) + put(CalendarContract.Calendars.IS_PRIMARY, 0) + put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0) + put(CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE, 1) + // from Android docs: "the device will only process METHOD_DEFAULT and METHOD_ALERT reminders" + put( + CalendarContract.Calendars.ALLOWED_REMINDERS, + CalendarContract.Reminders.METHOD_ALERT.toString() + ) + put( + CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES, + CalendarContract.Attendees.TYPE_NONE.toString() + ) + } + } + + /** + * Build content values needed to insert calendar colors + */ + private fun buildLocalCalendarColorsContentValues( + accountName: String, + colorType: Int, + colorKey: String, + color: Int + ): ContentValues { + return ContentValues().apply { + put(CalendarContract.Colors.ACCOUNT_NAME, accountName) + put(CalendarContract.Colors.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) + put(CalendarContract.Colors.COLOR_TYPE, colorType) + put(CalendarContract.Colors.COLOR_KEY, colorKey) + put(CalendarContract.Colors.COLOR, color) + } + } + + /** + * TODO Figure out exactly what this does + */ + private fun areCalendarColorsExisting(accountName: String): Boolean { + contentResolver.query( + CalendarContract.Colors.CONTENT_URI, + null, + CalendarContract.Colors.ACCOUNT_NAME + "=? AND " + CalendarContract.Colors.ACCOUNT_TYPE + "=?", + arrayOf(accountName, CalendarContract.ACCOUNT_TYPE_LOCAL), + null + ).use { + if (it!!.moveToFirst()) { + return true + } + } + return false + } + + /** + * TODO Figure out why is this a maybe? + */ + private fun maybeAddCalendarAndEventColors(accountName: String) { + if (areCalendarColorsExisting(accountName)) { + return + } + + val defaultColors: IntArray = + application.resources.getIntArray(R.array.defaultCalendarColors) + + val insertBulk = mutableListOf() + for ((i, color) in defaultColors.withIndex()) { + val colorKey = i.toString() + val colorCvCalendar = buildLocalCalendarColorsContentValues( + accountName, + CalendarContract.Colors.TYPE_CALENDAR, + colorKey, + color + ) + val colorCvEvent = buildLocalCalendarColorsContentValues( + accountName, + CalendarContract.Colors.TYPE_EVENT, + colorKey, + color + ) + insertBulk.add(colorCvCalendar) + insertBulk.add(colorCvEvent) + } + contentResolver.bulkInsert( + CalendarRepository.asLocalCalendarSyncAdapter( + accountName, + CalendarContract.Colors.CONTENT_URI + ), insertBulk.toTypedArray() + ) + } + + /** + * TODO Document + */ + fun addLocalCalendar(accountName: String, displayName: String): Uri { + + maybeAddCalendarAndEventColors(accountName) + + val cv = buildLocalCalendarContentValues(accountName, displayName) + return contentResolver.insert( + CalendarRepository.asLocalCalendarSyncAdapter( + accountName, + CalendarContract.Calendars.CONTENT_URI + ), cv + ) + ?: throw IllegalArgumentException() + } + + /** + * TODO Document + */ + fun deleteLocalCalendar(accountName: String, id: Long): Boolean { + val calUri = ContentUris.withAppendedId( + CalendarRepository.asLocalCalendarSyncAdapter( + accountName, + CalendarContract.Calendars.CONTENT_URI + ), id + ) + return contentResolver.delete(calUri, null, null) == 1 + } + + companion object { + private val PROJECTION = arrayOf( + CalendarContract.Calendars._ID, + CalendarContract.Calendars.ACCOUNT_NAME, + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.Calendars.OWNER_ACCOUNT, + CalendarContract.Calendars.NAME, + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, + CalendarContract.Calendars.CALENDAR_COLOR, + CalendarContract.Calendars.VISIBLE, + CalendarContract.Calendars.SYNC_EVENTS, + CalendarContract.Calendars.IS_PRIMARY + ) + private const val PROJECTION_INDEX_ID = 0 + private const val PROJECTION_INDEX_ACCOUNT_NAME = 1 + private const val PROJECTION_INDEX_ACCOUNT_TYPE = 2 + private const val PROJECTION_INDEX_OWNER_ACCOUNT = 3 + private const val PROJECTION_INDEX_NAME = 4 + private const val PROJECTION_INDEX_CALENDAR_DISPLAY_NAME = 5 + private const val PROJECTION_INDEX_CALENDAR_COLOR = 6 + private const val PROJECTION_INDEX_VISIBLE = 7 + private const val PROJECTION_INDEX_SYNC_EVENTS = 8 + private const val PROJECTION_INDEX_IS_PRIMARY = 9 + + private const val DEFAULT_COLOR_KEY = "1" + } +} diff --git a/app/src/main/java/com/android/calendar/datasource/EventDataSource.kt b/app/src/main/java/com/android/calendar/datasource/EventDataSource.kt new file mode 100644 index 00000000..5b5ee07c --- /dev/null +++ b/app/src/main/java/com/android/calendar/datasource/EventDataSource.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 The Etar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.android.calendar.datasource + +import android.app.Application +import android.provider.CalendarContract + +/** + * Datasource of Event entities + */ +class EventDataSource( + private val application: Application +) { + /** + * TODO Document + */ + fun queryNumberOfEvents(calendarId: Long): Long? { + val args = arrayOf(calendarId.toString()) + application.contentResolver.query( + CalendarContract.Events.CONTENT_URI, + PROJECTION_COUNT_EVENTS, + WHERE_COUNT_EVENTS, args, null + )?.use { + if (it.moveToFirst()) { + return it.getLong(PROJECTION_COUNT_EVENTS_INDEX_COUNT) + } + } + return null + } + + companion object { + private val PROJECTION_COUNT_EVENTS = arrayOf( + CalendarContract.Events._COUNT + ) + private const val PROJECTION_COUNT_EVENTS_INDEX_COUNT = 0 + private const val WHERE_COUNT_EVENTS = CalendarContract.Events.CALENDAR_ID + "=?" + } +} diff --git a/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt b/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt index 0fa3d73a..7bf153ad 100644 --- a/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt +++ b/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Dominik Schürmann + * Copyright (c) 2024 The Etar Project, Dominik Schürmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -12,7 +12,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * along with this program. If not, see . */ package com.android.calendar.persistence @@ -20,212 +20,76 @@ package com.android.calendar.persistence import android.accounts.Account import android.annotation.SuppressLint import android.app.Application -import android.content.ContentUris -import android.content.ContentValues -import android.content.Context import android.net.Uri import android.provider.CalendarContract -import androidx.lifecycle.LiveData -import ws.xsoh.etar.R +import com.android.calendar.datasource.AccountDataSource +import com.android.calendar.datasource.CalendarDataSource +import com.android.calendar.datasource.EventDataSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn /** + * Source of truth for everything related to Calendars. + * * Repository as in * https://developer.android.com/jetpack/docs/guide#recommended-app-arch * * TODO: * Replace usages of AsyncQueryService in Etar with repositories * Currently CalendarRepository is only used for settings + * + * TODO Move to a proper /repository folder */ @SuppressLint("MissingPermission") -internal class CalendarRepository(val application: Application) { - - private var contentResolver = application.contentResolver - - private var allCalendars: CalendarLiveData = CalendarLiveData(application.applicationContext) - - fun getCalendarsOrderedByAccount(): LiveData> { - return allCalendars - } - - class CalendarLiveData(val context: Context) : ContentProviderLiveData>(context, uri) { - - override fun getContentProviderValue(): List { - val calendars: MutableList = mutableListOf() - - context.contentResolver.query(uri, PROJECTION, null, null, CalendarContract.Calendars.ACCOUNT_NAME)?.use { - while (it.moveToNext()) { - val id = it.getLong(PROJECTION_INDEX_ID) - val accountName = it.getString(PROJECTION_INDEX_ACCOUNT_NAME) - val accountType = it.getString(PROJECTION_INDEX_ACCOUNT_TYPE) - val name = it.getString(PROJECTION_INDEX_NAME) - val displayName = it.getString(PROJECTION_INDEX_CALENDAR_DISPLAY_NAME) - val color = it.getInt(PROJECTION_INDEX_CALENDAR_COLOR) - val visible = it.getInt(PROJECTION_INDEX_VISIBLE) == 1 - val syncEvents = it.getInt(PROJECTION_INDEX_SYNC_EVENTS) == 1 - val isPrimary = it.getInt(PROJECTION_INDEX_IS_PRIMARY) == 1 - val isLocal = accountType == CalendarContract.ACCOUNT_TYPE_LOCAL - - calendars.add(Calendar(id, accountName, accountType, name, displayName, color, visible, syncEvents, isPrimary, isLocal)) - } - } - return calendars - } - - companion object { - private val uri = CalendarContract.Calendars.CONTENT_URI - - private val PROJECTION = arrayOf( - CalendarContract.Calendars._ID, - CalendarContract.Calendars.ACCOUNT_NAME, - CalendarContract.Calendars.ACCOUNT_TYPE, - CalendarContract.Calendars.OWNER_ACCOUNT, - CalendarContract.Calendars.NAME, - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, - CalendarContract.Calendars.CALENDAR_COLOR, - CalendarContract.Calendars.VISIBLE, - CalendarContract.Calendars.SYNC_EVENTS, - CalendarContract.Calendars.IS_PRIMARY - ) - const val PROJECTION_INDEX_ID = 0 - const val PROJECTION_INDEX_ACCOUNT_NAME = 1 - const val PROJECTION_INDEX_ACCOUNT_TYPE = 2 - const val PROJECTION_INDEX_OWNER_ACCOUNT = 3 - const val PROJECTION_INDEX_NAME = 4 - const val PROJECTION_INDEX_CALENDAR_DISPLAY_NAME = 5 - const val PROJECTION_INDEX_CALENDAR_COLOR = 6 - const val PROJECTION_INDEX_VISIBLE = 7 - const val PROJECTION_INDEX_SYNC_EVENTS = 8 - const val PROJECTION_INDEX_IS_PRIMARY = 9 - } - } - - private fun buildLocalCalendarContentValues(accountName: String, displayName: String): ContentValues { - val internalName = "etar_local_" + displayName.replace("[^a-zA-Z0-9]".toRegex(), "") - return ContentValues().apply { - put(CalendarContract.Calendars.ACCOUNT_NAME, accountName) - put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - put(CalendarContract.Calendars.OWNER_ACCOUNT, accountName) - put(CalendarContract.Calendars.NAME, internalName) - put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName) - put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, DEFAULT_COLOR_KEY) - put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_ROOT) - put(CalendarContract.Calendars.VISIBLE, 1) - put(CalendarContract.Calendars.SYNC_EVENTS, 1) - put(CalendarContract.Calendars.IS_PRIMARY, 0) - put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0) - put(CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE, 1) - // from Android docs: "the device will only process METHOD_DEFAULT and METHOD_ALERT reminders" - put(CalendarContract.Calendars.ALLOWED_REMINDERS, CalendarContract.Reminders.METHOD_ALERT.toString()) - put(CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES, CalendarContract.Attendees.TYPE_NONE.toString()) - } - } - - private fun buildLocalCalendarColorsContentValues(accountName: String, colorType: Int, colorKey: String, color: Int): ContentValues { - return ContentValues().apply { - put(CalendarContract.Colors.ACCOUNT_NAME, accountName) - put(CalendarContract.Colors.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - put(CalendarContract.Colors.COLOR_TYPE, colorType) - put(CalendarContract.Colors.COLOR_KEY, colorKey) - put(CalendarContract.Colors.COLOR, color) - } - } - - fun addLocalCalendar(accountName: String, displayName: String): Uri { - maybeAddCalendarAndEventColors(accountName) - - val cv = buildLocalCalendarContentValues(accountName, displayName) - return contentResolver.insert(asLocalCalendarSyncAdapter(accountName, CalendarContract.Calendars.CONTENT_URI), cv) - ?: throw IllegalArgumentException() - } - - private fun maybeAddCalendarAndEventColors(accountName: String) { - if (areCalendarColorsExisting(accountName)) { - return - } - - val defaultColors: IntArray = application.resources.getIntArray(R.array.defaultCalendarColors) - - val insertBulk = mutableListOf() - for ((i, color) in defaultColors.withIndex()) { - val colorKey = i.toString() - val colorCvCalendar = buildLocalCalendarColorsContentValues(accountName, CalendarContract.Colors.TYPE_CALENDAR, colorKey, color) - val colorCvEvent = buildLocalCalendarColorsContentValues(accountName, CalendarContract.Colors.TYPE_EVENT, colorKey, color) - insertBulk.add(colorCvCalendar) - insertBulk.add(colorCvEvent) - } - contentResolver.bulkInsert(asLocalCalendarSyncAdapter(accountName, CalendarContract.Colors.CONTENT_URI), insertBulk.toTypedArray()) - } - - private fun areCalendarColorsExisting(accountName: String): Boolean { - contentResolver.query(CalendarContract.Colors.CONTENT_URI, - null, - CalendarContract.Colors.ACCOUNT_NAME + "=? AND " + CalendarContract.Colors.ACCOUNT_TYPE + "=?", - arrayOf(accountName, CalendarContract.ACCOUNT_TYPE_LOCAL), - null).use { - if (it!!.moveToFirst()) { - return true - } - } - return false - } +internal class CalendarRepository(val application: Application) : ICalendarRepository { + /** + * Source of calendar entities + */ + private val calendarDataSource = CalendarDataSource(application) /** - * @return true iff exactly one row is deleted + * Source of event entities */ - fun deleteLocalCalendar(accountName: String, id: Long): Boolean { - val calUri = ContentUris.withAppendedId(asLocalCalendarSyncAdapter(accountName, CalendarContract.Calendars.CONTENT_URI), id) - return contentResolver.delete(calUri, null, null) == 1 - } + private val eventDataSource = EventDataSource(application) - fun queryAccount(calendarId: Long): Account? { - val calendarUri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId) - contentResolver.query(calendarUri, ACCOUNT_PROJECTION, null, null, null)?.use { - if (it.moveToFirst()) { - val accountName = it.getString(PROJECTION_ACCOUNT_INDEX_NAME) - val accountType = it.getString(PROJECTION_ACCOUNT_INDEX_TYPE) - return Account(accountName, accountType) - } - } - return null - } + /** + * Source of account entities + */ + private val accountDataSource = AccountDataSource(application) - fun queryNumberOfEvents(calendarId: Long): Long? { - val args = arrayOf(calendarId.toString()) - contentResolver.query(CalendarContract.Events.CONTENT_URI, PROJECTION_COUNT_EVENTS, WHERE_COUNT_EVENTS, args, null)?.use { - if (it.moveToFirst()) { - return it.getLong(PROJECTION_COUNT_EVENTS_INDEX_COUNT) - } - } - return null - } + override fun getCalendarsOrderedByAccount(): Flow> = + calendarDataSource.getAllCalendars().flowOn(Dispatchers.IO) + + override fun addLocalCalendar(accountName: String, displayName: String): Uri = + calendarDataSource.addLocalCalendar(accountName, displayName) + + override fun deleteLocalCalendar(accountName: String, id: Long): Boolean = + calendarDataSource.deleteLocalCalendar(accountName, id) + + override fun queryAccount(calendarId: Long): Account? = + accountDataSource.queryAccount(calendarId) + + override fun queryNumberOfEvents(calendarId: Long): Long? = + eventDataSource.queryNumberOfEvents(calendarId) companion object { - private val ACCOUNT_PROJECTION = arrayOf( - CalendarContract.Calendars.ACCOUNT_NAME, - CalendarContract.Calendars.ACCOUNT_TYPE - ) - const val PROJECTION_ACCOUNT_INDEX_NAME = 0 - const val PROJECTION_ACCOUNT_INDEX_TYPE = 1 - - private val PROJECTION_COUNT_EVENTS = arrayOf( - CalendarContract.Events._COUNT - ) - const val PROJECTION_COUNT_EVENTS_INDEX_COUNT = 0 - const val WHERE_COUNT_EVENTS = CalendarContract.Events.CALENDAR_ID + "=?" - - const val DEFAULT_COLOR_KEY = "1" /** * Operations only work if they are made "under" the correct account + * + * TODO Find a better place for this function */ @JvmStatic fun asLocalCalendarSyncAdapter(accountName: String, uri: Uri): Uri { return uri.buildUpon() - .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, accountName) - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL).build() + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, accountName) + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.ACCOUNT_TYPE_LOCAL + ).build() } } - } diff --git a/app/src/main/java/com/android/calendar/persistence/ContentProviderLiveData.kt b/app/src/main/java/com/android/calendar/persistence/ContentProviderLiveData.kt deleted file mode 100644 index 448168f8..00000000 --- a/app/src/main/java/com/android/calendar/persistence/ContentProviderLiveData.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2020 Dominik Schürmann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.android.calendar.persistence - -import android.content.Context -import android.database.ContentObserver -import android.net.Uri -import androidx.lifecycle.MutableLiveData -import com.android.calendar.Utils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch - -/** - * Based on https://medium.com/@jmcassis/android-livedata-and-content-provider-updates-5f8fd3b2b3a4 - * - * Abstract [LiveData] to observe Android's Content Provider changes. - * Provide a [uri] to observe changes and implement [getContentProviderValue] - * to provide data to post when content provider notifies a change. - */ -abstract class ContentProviderLiveData( - private val context: Context, - private val uri: Uri -) : MutableLiveData() { - - private var observer = object : ContentObserver(null) { - override fun onChange(self: Boolean) { - // Notify LiveData listeners that data at the uri has changed - getContentProviderValueAsync() - } - } - - override fun onActive() { - if (Utils.isCalendarPermissionGranted(context, true)) { - context.contentResolver.registerContentObserver(uri, true, observer) - getContentProviderValueAsync() - } - } - - override fun onInactive() { - context.contentResolver.unregisterContentObserver(observer) - } - - private fun getContentProviderValueAsync() { - GlobalScope.launch(Dispatchers.Main) { - val accounts = async { - getContentProviderValue() - } - - postValue(accounts.await()) - } - } - - /** - * Implement if you need to provide [T] value to be posted - * when observed content is changed. - */ - abstract fun getContentProviderValue(): T -} diff --git a/app/src/main/java/com/android/calendar/persistence/ICalendarRepository.kt b/app/src/main/java/com/android/calendar/persistence/ICalendarRepository.kt new file mode 100644 index 00000000..cb009b77 --- /dev/null +++ b/app/src/main/java/com/android/calendar/persistence/ICalendarRepository.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 The Etar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.android.calendar.persistence + +import android.accounts.Account +import android.app.Application +import android.net.Uri +import kotlinx.coroutines.flow.Flow + +/** + * Interface for a Source of truth of all things Calendars. + * + * TODO Move to proper /repository folder + */ +interface ICalendarRepository { + + /** + * Get flow of calendars order by their account. + * + * Automatically updates when collected, disconnects afterwards. + * + * TODO Better documentation, idk if this actually orders or not. + */ + fun getCalendarsOrderedByAccount(): Flow> + + /** + * TODO document + */ + fun addLocalCalendar(accountName: String, displayName: String): Uri + + /** + * TODO document better + * + * @return true iff exactly one row is deleted + */ + fun deleteLocalCalendar(accountName: String, id: Long): Boolean + + /** + * Query the owning account of a given calendar + */ + fun queryAccount(calendarId: Long): Account? + + /** + * TODO document + */ + fun queryNumberOfEvents(calendarId: Long): Long? + + companion object { + /** + * Static repository holder. + * + * We hold this, since repositories are meant to be singletons. + */ + private var static: ICalendarRepository? = null + + /** + * TODO Replace with proper dependency injection + */ + fun get(application: Application): ICalendarRepository { + if (static == null) { + static = CalendarRepository(application) + } + + return static!! + } + } +} diff --git a/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt b/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt index 4743d602..a5f71ebb 100644 --- a/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt +++ b/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt @@ -36,20 +36,20 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference import com.android.calendar.Utils import com.android.calendar.alerts.channelId -import com.android.calendar.persistence.CalendarRepository +import com.android.calendar.persistence.ICalendarRepository import ws.xsoh.etar.R class CalendarPreferences : PreferenceFragmentCompat() { private var calendarId: Long = -1 - private lateinit var calendarRepository: CalendarRepository + private lateinit var calendarRepository: ICalendarRepository private lateinit var account: Account private var numberOfEvents: Long = -1 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { calendarId = requireArguments().getLong(ARG_CALENDAR_ID) - calendarRepository = CalendarRepository(requireActivity().application) + calendarRepository = ICalendarRepository.get(requireActivity().application) account = calendarRepository.queryAccount(calendarId)!! numberOfEvents = calendarRepository.queryNumberOfEvents(calendarId)!! diff --git a/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt b/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt index b44161c0..93d8ca28 100644 --- a/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt +++ b/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt @@ -20,13 +20,14 @@ package com.android.calendar.settings import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData import com.android.calendar.persistence.Calendar -import com.android.calendar.persistence.CalendarRepository +import com.android.calendar.persistence.ICalendarRepository class MainListViewModel(application: Application) : AndroidViewModel(application) { - private var repository: CalendarRepository = CalendarRepository(application) + private var repository = ICalendarRepository.get(application) // Using LiveData and caching what fetchCalendarsByAccountName returns has several benefits: // - We can put an observer on the data (instead of polling for changes) and only update the @@ -39,7 +40,7 @@ class MainListViewModel(application: Application) : AndroidViewModel(application } private fun loadCalendars() { - allCalendars = repository.getCalendarsOrderedByAccount() + allCalendars = repository.getCalendarsOrderedByAccount().asLiveData() } fun getCalendarsOrderedByAccount(): LiveData> {