mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2025-12-23 23:17:50 -05:00
AndroidCalendar refactoring (#1560)
* [WIP] Refactor calendar sync manager to use synctools library * [WIP] Update synctools * [WIP] Tests * Remove test logger module and update calendar color methods * Fix migrations * Update libs.versions.toml
This commit is contained in:
@@ -11,7 +11,11 @@ import android.os.Bundle
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import at.bitfire.davdroid.di.TestCoroutineDispatchersModule
|
||||
import at.bitfire.davdroid.sync.SyncAdapterService
|
||||
import at.bitfire.davdroid.test.BuildConfig
|
||||
import at.bitfire.synctools.log.LogcatHandler
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Suppress("unused")
|
||||
class HiltTestRunner : AndroidJUnitRunner() {
|
||||
@@ -22,6 +26,12 @@ class HiltTestRunner : AndroidJUnitRunner() {
|
||||
override fun onCreate(arguments: Bundle?) {
|
||||
super.onCreate(arguments)
|
||||
|
||||
// set root logger to adb Logcat
|
||||
val rootLogger = Logger.getLogger("")
|
||||
rootLogger.level = Level.ALL
|
||||
rootLogger.handlers.forEach { rootLogger.removeHandler(it) }
|
||||
rootLogger.addHandler(LogcatHandler(BuildConfig.APPLICATION_ID))
|
||||
|
||||
// MockK requirements
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||
throw AssertionError("MockK requires Android P [https://mockk.io/ANDROID.html]")
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.di
|
||||
|
||||
import at.bitfire.davdroid.log.LogcatHandler
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Module that provides verbose logging for tests.
|
||||
*/
|
||||
@TestInstallIn(
|
||||
components = [SingletonComponent::class],
|
||||
replaces = [LoggerModule::class]
|
||||
)
|
||||
@Module
|
||||
class TestLoggerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun logger(): Logger = Logger.getGlobal().apply {
|
||||
level = Level.ALL
|
||||
addHandler(LogcatHandler())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,11 +12,11 @@ import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidEvent
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
|
||||
import at.bitfire.ical4android.util.MiscUtils.closeCompat
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
|
||||
import at.bitfire.synctools.test.InitCalendarProviderRule
|
||||
import net.fortuna.ical4j.model.property.DtStart
|
||||
import net.fortuna.ical4j.model.property.RRule
|
||||
@@ -37,21 +37,21 @@ class LocalCalendarTest {
|
||||
|
||||
@JvmField
|
||||
@ClassRule
|
||||
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance()
|
||||
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.initialize()
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
private lateinit var client: ContentProviderClient
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setUpClass() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
|
||||
client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun tearDownClass() {
|
||||
provider.closeCompat()
|
||||
client.closeCompat()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,8 +61,8 @@ class LocalCalendarTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val uri = AndroidCalendar.create(account, provider, ContentValues())
|
||||
calendar = LocalCalendar(AndroidCalendar.findByID(account, provider, ContentUris.parseId(uri)))
|
||||
val provider = AndroidCalendarProvider(account, client)
|
||||
calendar = LocalCalendar(provider.createAndGetCalendar(ContentValues()))
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -102,7 +102,7 @@ class LocalCalendarTest {
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
@@ -110,7 +110,7 @@ class LocalCalendarTest {
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is now marked as deleted
|
||||
provider.query(
|
||||
client.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
@@ -132,7 +132,7 @@ class LocalCalendarTest {
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
@@ -140,7 +140,7 @@ class LocalCalendarTest {
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is not marked as deleted
|
||||
provider.query(
|
||||
client.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
|
||||
@@ -12,10 +12,10 @@ import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidEvent
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.util.MiscUtils.closeCompat
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
|
||||
import at.techbee.jtx.JtxContract.asSyncAdapter
|
||||
import net.fortuna.ical4j.model.property.DtStart
|
||||
import net.fortuna.ical4j.model.property.RRule
|
||||
@@ -36,8 +36,8 @@ class LocalEventTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val uri = AndroidCalendar.create(account, provider, ContentValues())
|
||||
calendar = LocalCalendar(AndroidCalendar.findByID(account, provider, ContentUris.parseId(uri)))
|
||||
val provider = AndroidCalendarProvider(account, client)
|
||||
calendar = LocalCalendar(provider.createAndGetCalendar(ContentValues()))
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -64,7 +64,7 @@ class LocalEventTest {
|
||||
UUID.fromString(fileName)
|
||||
|
||||
// UID in calendar storage should be the same as file name
|
||||
provider.query(
|
||||
client.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
|
||||
arrayOf(Events.UID_2445), null, null, null
|
||||
)!!.use { cursor ->
|
||||
@@ -91,7 +91,7 @@ class LocalEventTest {
|
||||
assertEquals(event.uid, fileName)
|
||||
|
||||
// UID in calendar storage should still be set, too
|
||||
provider.query(
|
||||
client.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
|
||||
arrayOf(Events.UID_2445), null, null, null
|
||||
)!!.use { cursor ->
|
||||
@@ -119,7 +119,7 @@ class LocalEventTest {
|
||||
UUID.fromString(fileName)
|
||||
|
||||
// UID in calendar storage shouldn't have been changed
|
||||
provider.query(
|
||||
client.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
|
||||
arrayOf(Events.UID_2445), null, null, null
|
||||
)!!.use { cursor ->
|
||||
@@ -165,7 +165,7 @@ class LocalEventTest {
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
@@ -173,7 +173,7 @@ class LocalEventTest {
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is now marked as deleted
|
||||
provider.query(
|
||||
client.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
@@ -194,7 +194,7 @@ class LocalEventTest {
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
@@ -202,7 +202,7 @@ class LocalEventTest {
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is not marked as deleted
|
||||
provider.query(
|
||||
client.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
@@ -214,19 +214,19 @@ class LocalEventTest {
|
||||
|
||||
companion object {
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
private lateinit var client: ContentProviderClient
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setUpClass() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
|
||||
client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun tearDownClass() {
|
||||
provider.closeCompat()
|
||||
client.closeCompat()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -127,16 +127,16 @@ class AccountSettingsMigration20Test {
|
||||
Calendars.NAME to url,
|
||||
Calendars.SYNC_EVENTS to 1
|
||||
)
|
||||
)!!
|
||||
)!!.asSyncAdapter(account)
|
||||
try {
|
||||
migration.migrateCalendars(account, calDavServiceId = 1)
|
||||
migration.migrateCalendars(account, 1)
|
||||
|
||||
provider.query(uri.asSyncAdapter(account), arrayOf(Calendars._SYNC_ID), null, null, null)!!.use { cursor ->
|
||||
provider.query(uri, arrayOf(Calendars._SYNC_ID), null, null, null)!!.use { cursor ->
|
||||
cursor.moveToNext()
|
||||
assertEquals(collectionId, cursor.getLongOrNull(0))
|
||||
}
|
||||
} finally {
|
||||
provider.delete(uri.asSyncAdapter(account), null, null)
|
||||
provider.delete(uri, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.LogFileHandler.Companion.debugDir
|
||||
import at.bitfire.davdroid.ui.AppSettingsActivity
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import at.bitfire.davdroid.ui.NotificationRegistry
|
||||
import at.bitfire.synctools.log.PlainTextFormatter
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.repository.PreferenceRepository
|
||||
import at.bitfire.synctools.log.LogcatHandler
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -79,7 +80,7 @@ class LogManager @Inject constructor(
|
||||
// root logger: set default log level and always log to logcat
|
||||
val rootLogger = Logger.getLogger("")
|
||||
rootLogger.level = if (logVerbose) Level.ALL else Level.INFO
|
||||
rootLogger.addHandler(LogcatHandler())
|
||||
rootLogger.addHandler(LogcatHandler(BuildConfig.APPLICATION_ID))
|
||||
|
||||
// log to file, if requested
|
||||
if (logToFile)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import com.google.common.base.Ascii
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
/**
|
||||
* Logging handler that logs to Android logcat.
|
||||
*/
|
||||
internal class LogcatHandler: Handler() {
|
||||
|
||||
init {
|
||||
formatter = PlainTextFormatter.LOGCAT
|
||||
}
|
||||
|
||||
override fun publish(r: LogRecord) {
|
||||
val level = r.level.intValue()
|
||||
val text = formatter.format(r)
|
||||
|
||||
// get class name that calls the logger (or fall back to package name)
|
||||
val className = if (r.sourceClassName != null)
|
||||
PlainTextFormatter.shortClassName(r.sourceClassName)
|
||||
else
|
||||
BuildConfig.APPLICATION_ID
|
||||
|
||||
// truncate class name to 23 characters on Android <8, see Log documentation
|
||||
val tag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
Ascii.truncate(className, 23, "")
|
||||
else
|
||||
className
|
||||
|
||||
when {
|
||||
level >= Level.SEVERE.intValue() -> Log.e(tag, text, r.thrown)
|
||||
level >= Level.WARNING.intValue() -> Log.w(tag, text, r.thrown)
|
||||
level >= Level.CONFIG.intValue() -> Log.i(tag, text, r.thrown)
|
||||
level >= Level.FINER.intValue() -> Log.d(tag, text, r.thrown)
|
||||
else -> Log.v(tag, text, r.thrown)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {}
|
||||
override fun close() {}
|
||||
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import com.google.common.base.Ascii
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class PlainTextFormatter(
|
||||
private val withTime: Boolean,
|
||||
private val withSource: Boolean,
|
||||
private val padSource: Int = 30,
|
||||
private val withException: Boolean,
|
||||
private val lineSeparator: String?
|
||||
): Formatter() {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Formatter intended for logcat output.
|
||||
*/
|
||||
val LOGCAT = PlainTextFormatter(
|
||||
withTime = false,
|
||||
withSource = false,
|
||||
withException = false,
|
||||
lineSeparator = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Formatter intended for file output.
|
||||
*/
|
||||
val DEFAULT = PlainTextFormatter(
|
||||
withTime = true,
|
||||
withSource = true,
|
||||
withException = true,
|
||||
lineSeparator = System.lineSeparator()
|
||||
)
|
||||
|
||||
/**
|
||||
* Maximum length of a log line (estimate).
|
||||
*/
|
||||
const val MAX_LENGTH = 10000
|
||||
|
||||
fun shortClassName(className: String) = className
|
||||
.replace(Regex("^at\\.bitfire\\.(dav|cert4an|dav4an|ical4an|vcard4an)droid\\."), ".")
|
||||
.replace(Regex("\\$.*$"), "")
|
||||
|
||||
private fun stackTrace(ex: Throwable): String {
|
||||
val writer = StringWriter()
|
||||
ex.printStackTrace(PrintWriter(writer))
|
||||
return writer.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT)
|
||||
|
||||
|
||||
override fun format(r: LogRecord): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
if (withTime)
|
||||
builder .append(timeFormat.format(Date(r.millis)))
|
||||
.append(" ").append(r.threadID).append(" ")
|
||||
|
||||
if (withSource && r.sourceClassName != null) {
|
||||
val className = shortClassName(r.sourceClassName)
|
||||
if (className != r.loggerName) {
|
||||
val classNameColumn = "[$className] ".padEnd(padSource)
|
||||
builder.append(classNameColumn)
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(truncate(r.message))
|
||||
|
||||
if (withException && r.thrown != null) {
|
||||
val indentedStackTrace = stackTrace(r.thrown)
|
||||
.replace("\n", "\n\t")
|
||||
.removeSuffix("\t")
|
||||
builder.append("\n\tEXCEPTION ").append(indentedStackTrace)
|
||||
}
|
||||
|
||||
r.parameters?.let {
|
||||
for ((idx, param) in it.withIndex()) {
|
||||
builder.append("\n\tPARAMETER #").append(idx + 1).append(" = ")
|
||||
|
||||
val valStr = if (param == null)
|
||||
"(null)"
|
||||
else
|
||||
truncate(param.toString())
|
||||
builder.append(valStr)
|
||||
}
|
||||
}
|
||||
|
||||
if (lineSeparator != null)
|
||||
builder.append(lineSeparator)
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun truncate(s: String) =
|
||||
Ascii.truncate(s, MAX_LENGTH, "[…]")
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import at.bitfire.synctools.log.PlainTextFormatter
|
||||
import com.google.common.base.Ascii
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
@@ -9,11 +9,11 @@ import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.core.content.contentValuesOf
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidEvent
|
||||
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
|
||||
import at.bitfire.synctools.storage.BatchOperation
|
||||
import at.bitfire.synctools.storage.CalendarBatchOperation
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendar
|
||||
import at.bitfire.synctools.storage.calendar.CalendarBatchOperation
|
||||
import java.util.LinkedList
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
@@ -41,17 +41,20 @@ class LocalCalendar(
|
||||
get() = androidCalendar.displayName ?: androidCalendar.id.toString()
|
||||
|
||||
override val readOnly
|
||||
get() = androidCalendar.accessLevel?.let { it <= Calendars.CAL_ACCESS_READ } ?: false
|
||||
get() = androidCalendar.accessLevel <= Calendars.CAL_ACCESS_READ
|
||||
|
||||
override var lastSyncState: SyncState?
|
||||
get() = androidCalendar.readSyncState()?.let { SyncState.fromString(it) }
|
||||
get() = androidCalendar.readSyncState()?.let {
|
||||
SyncState.fromString(it)
|
||||
}
|
||||
set(state) {
|
||||
androidCalendar.writeSyncState(state.toString())
|
||||
}
|
||||
|
||||
|
||||
override fun findDeleted() =
|
||||
androidCalendar.queryEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null)
|
||||
androidCalendar
|
||||
.findEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null)
|
||||
.map { LocalEvent(it) }
|
||||
|
||||
override fun findDirty(): List<LocalEvent> {
|
||||
@@ -62,7 +65,7 @@ class LocalCalendar(
|
||||
* When a calendar component is created, its sequence number is 0. It is monotonically incremented by the "Organizer's"
|
||||
* CUA each time the "Organizer" makes a significant revision to the calendar component.
|
||||
*/
|
||||
for (androidEvent in androidCalendar.queryEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null)) {
|
||||
for (androidEvent in androidCalendar.findEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null)) {
|
||||
val localEvent = LocalEvent(androidEvent)
|
||||
try {
|
||||
val event = requireNotNull(androidEvent.event)
|
||||
@@ -87,44 +90,38 @@ class LocalCalendar(
|
||||
}
|
||||
|
||||
override fun findByName(name: String) =
|
||||
androidCalendar.queryEvents("${Events._SYNC_ID}=?", arrayOf(name)).firstOrNull()?.let { LocalEvent(it) }
|
||||
androidCalendar.findEvents("${Events._SYNC_ID}=?", arrayOf(name)).firstOrNull()?.let { LocalEvent(it) }
|
||||
|
||||
|
||||
override fun markNotDirty(flags: Int): Int {
|
||||
val values = contentValuesOf(AndroidEvent.COLUMN_FLAGS to flags)
|
||||
return androidCalendar.provider.update(
|
||||
Events.CONTENT_URI.asSyncAdapter(androidCalendar.account), values,
|
||||
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL",
|
||||
override fun markNotDirty(flags: Int) =
|
||||
androidCalendar.updateEvents(
|
||||
contentValuesOf(AndroidEvent.COLUMN_FLAGS to flags),
|
||||
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL",
|
||||
arrayOf(androidCalendar.id.toString())
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeNotDirtyMarked(flags: Int): Int {
|
||||
var deleted = 0
|
||||
// list all non-dirty events with the given flags and delete every row + its exceptions
|
||||
androidCalendar.provider.query(
|
||||
Events.CONTENT_URI.asSyncAdapter(androidCalendar.account), arrayOf(Events._ID),
|
||||
val batch = CalendarBatchOperation(androidCalendar.client)
|
||||
androidCalendar.iterateEvents(
|
||||
arrayOf(Events._ID),
|
||||
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${AndroidEvent.COLUMN_FLAGS}=?",
|
||||
arrayOf(androidCalendar.id.toString(), flags.toString()), null
|
||||
)?.use { cursor ->
|
||||
val batch = CalendarBatchOperation(androidCalendar.provider)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(0)
|
||||
// delete event and possible exceptions (content provider doesn't delete exceptions itself)
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newDelete(Events.CONTENT_URI.asSyncAdapter(androidCalendar.account))
|
||||
.withSelection("${Events._ID}=? OR ${Events.ORIGINAL_ID}=?", arrayOf(id.toString(), id.toString()))
|
||||
}
|
||||
deleted = batch.commit()
|
||||
arrayOf(androidCalendar.id.toString(), flags.toString())
|
||||
) { values ->
|
||||
val id = values.getAsInteger(Events._ID)
|
||||
|
||||
// delete event and possible exceptions (content provider doesn't delete exceptions itself)
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newDelete(Events.CONTENT_URI.asSyncAdapter(androidCalendar.account))
|
||||
.withSelection("${Events._ID}=? OR ${Events.ORIGINAL_ID}=?", arrayOf(id.toString(), id.toString()))
|
||||
}
|
||||
return deleted
|
||||
return batch.commit()
|
||||
}
|
||||
|
||||
override fun forgetETags() {
|
||||
val values = contentValuesOf(AndroidEvent.COLUMN_ETAG to null)
|
||||
androidCalendar.provider.update(
|
||||
Events.CONTENT_URI.asSyncAdapter(androidCalendar.account), values, "${Events.CALENDAR_ID}=?",
|
||||
arrayOf(androidCalendar.id.toString())
|
||||
androidCalendar.updateEvents(
|
||||
contentValuesOf(AndroidEvent.COLUMN_ETAG to null),
|
||||
"${Events.CALENDAR_ID}=?", arrayOf(androidCalendar.id.toString())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,68 +129,60 @@ class LocalCalendar(
|
||||
fun processDirtyExceptions() {
|
||||
// process deleted exceptions
|
||||
logger.info("Processing deleted exceptions")
|
||||
androidCalendar.provider.query(
|
||||
Events.CONTENT_URI.asSyncAdapter(androidCalendar.account),
|
||||
|
||||
androidCalendar.iterateEvents(
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(androidCalendar.id.toString()), null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
logger.fine("Found deleted exception, removing and re-scheduling original event (if available)")
|
||||
val id = cursor.getLong(0) // can't be null (by definition)
|
||||
val originalID = cursor.getLong(1) // can't be null (by query)
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(androidCalendar.id.toString())
|
||||
) { values ->
|
||||
logger.fine("Found deleted exception, removing and re-scheduling original event (if available)")
|
||||
|
||||
val batch = CalendarBatchOperation(androidCalendar.provider)
|
||||
val id = values.getAsLong(Events._ID) // can't be null (by definition)
|
||||
val originalID = values.getAsLong(Events.ORIGINAL_ID) // can't be null (by query)
|
||||
|
||||
// get original event's SEQUENCE
|
||||
androidCalendar.provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(androidCalendar.account),
|
||||
arrayOf(AndroidEvent.COLUMN_SEQUENCE),
|
||||
null, null, null)?.use { cursor2 ->
|
||||
if (cursor2.moveToNext()) {
|
||||
// original event is available
|
||||
val originalSequence = if (cursor2.isNull(0)) 0 else cursor2.getInt(0)
|
||||
val batch = CalendarBatchOperation(androidCalendar.client)
|
||||
|
||||
// re-schedule original event and set it to DIRTY
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(androidCalendar.account))
|
||||
.withValue(AndroidEvent.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.withValue(Events.DIRTY, 1)
|
||||
}
|
||||
}
|
||||
// enqueue: increase sequence of main event
|
||||
val originalEventValues = androidCalendar.getEventValues(originalID, arrayOf(AndroidEvent.COLUMN_SEQUENCE))
|
||||
val originalSequence = originalEventValues?.getAsInteger(AndroidEvent.COLUMN_SEQUENCE) ?: 0
|
||||
|
||||
// completely remove deleted exception
|
||||
batch += BatchOperation.CpoBuilder.newDelete(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(androidCalendar.account))
|
||||
batch.commit()
|
||||
}
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(androidCalendar.account))
|
||||
.withValue(AndroidEvent.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.withValue(Events.DIRTY, 1)
|
||||
|
||||
// completely remove deleted exception
|
||||
batch += BatchOperation.CpoBuilder.newDelete(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(androidCalendar.account))
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
// process dirty exceptions
|
||||
logger.info("Processing dirty exceptions")
|
||||
androidCalendar.provider.query(
|
||||
Events.CONTENT_URI.asSyncAdapter(androidCalendar.account),
|
||||
androidCalendar.iterateEvents(
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(androidCalendar.id.toString()), null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
logger.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
|
||||
val id = cursor.getLong(0) // can't be null (by definition)
|
||||
val originalID = cursor.getLong(1) // can't be null (by query)
|
||||
val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2)
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(androidCalendar.id.toString())
|
||||
) { values ->
|
||||
logger.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
|
||||
|
||||
val batch = CalendarBatchOperation(androidCalendar.provider)
|
||||
// original event to DIRTY
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(androidCalendar.account))
|
||||
.withValue(Events.DIRTY, 1)
|
||||
// increase SEQUENCE and set DIRTY to 0
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(androidCalendar.account))
|
||||
.withValue(AndroidEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
batch.commit()
|
||||
}
|
||||
val id = values.getAsLong(Events._ID) // can't be null (by definition)
|
||||
val originalID = values.getAsLong(Events.ORIGINAL_ID) // can't be null (by query)
|
||||
val sequence = values.getAsInteger(AndroidEvent.COLUMN_SEQUENCE) ?: 0
|
||||
|
||||
val batch = CalendarBatchOperation(androidCalendar.client)
|
||||
|
||||
// enqueue: set original event to DIRTY
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(androidCalendar.eventUri(originalID))
|
||||
.withValue(Events.DIRTY, 1)
|
||||
|
||||
// enqueue: increase exception SEQUENCE and set DIRTY to 0
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(androidCalendar.eventUri(id))
|
||||
.withValue(AndroidEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,23 +192,21 @@ class LocalCalendar(
|
||||
* @return number of affected events
|
||||
*/
|
||||
fun deleteDirtyEventsWithoutInstances() {
|
||||
androidCalendar.provider.query(
|
||||
Events.CONTENT_URI.asSyncAdapter(androidCalendar.account),
|
||||
// Iterate dirty main events without exceptions
|
||||
androidCalendar.iterateEvents(
|
||||
arrayOf(Events._ID),
|
||||
"${Events.DIRTY} AND NOT ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", // Get dirty main events (and no exception events)
|
||||
null, null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val eventID = cursor.getLong(0)
|
||||
"${Events.DIRTY} AND NOT ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL",
|
||||
null
|
||||
) { values ->
|
||||
val eventID = values.getAsLong(Events._ID)
|
||||
|
||||
// get number of instances
|
||||
val numEventInstances = AndroidEvent.numInstances(androidCalendar.provider, androidCalendar.account, eventID)
|
||||
// get number of instances
|
||||
val numEventInstances = AndroidEvent.numInstances(androidCalendar.client, androidCalendar.account, eventID)
|
||||
|
||||
// delete event if there are no instances
|
||||
if (numEventInstances == 0) {
|
||||
logger.info("Marking event #$eventID without instances as deleted")
|
||||
AndroidEvent.markAsDeleted(androidCalendar.provider, androidCalendar.account, eventID)
|
||||
}
|
||||
// delete event if there are no instances
|
||||
if (numEventInstances == 0) {
|
||||
logger.fine("Marking event #$eventID without instances as deleted")
|
||||
AndroidEvent.markAsDeleted(androidCalendar.client, androidCalendar.account, eventID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ package at.bitfire.davdroid.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.Attendees
|
||||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Events
|
||||
import android.provider.CalendarContract.Reminders
|
||||
import androidx.core.content.contentValuesOf
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
@@ -18,10 +20,9 @@ import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.repository.DavServiceRepository
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.util.DavUtils.lastSegment
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidCalendar.Companion.calendarBaseValues
|
||||
import at.bitfire.ical4android.util.DateUtils
|
||||
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
@@ -40,7 +41,7 @@ class LocalCalendarStore @Inject constructor(
|
||||
override fun acquireContentProvider() =
|
||||
context.contentResolver.acquireContentProviderClient(authority)
|
||||
|
||||
override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalCalendar? {
|
||||
override fun create(client: ContentProviderClient, fromCollection: Collection): LocalCalendar? {
|
||||
val service = serviceRepository.getBlocking(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection")
|
||||
val account = Account(service.accountName, context.getString(R.string.account_type))
|
||||
|
||||
@@ -69,27 +70,49 @@ class LocalCalendarStore @Inject constructor(
|
||||
}
|
||||
|
||||
logger.log(Level.INFO, "Adding local calendar", values)
|
||||
val uri = AndroidCalendar.create(account, provider, values)
|
||||
return LocalCalendar(AndroidCalendar.findByID(account, provider, ContentUris.parseId(uri)))
|
||||
val provider = AndroidCalendarProvider(account, client)
|
||||
return LocalCalendar(provider.createAndGetCalendar(values))
|
||||
}
|
||||
|
||||
override fun getAll(account: Account, provider: ContentProviderClient) =
|
||||
AndroidCalendar.find(account, provider, "${Calendars.SYNC_EVENTS}!=0", null)
|
||||
override fun getAll(account: Account, client: ContentProviderClient) =
|
||||
AndroidCalendarProvider(account, client)
|
||||
.findCalendars("${Calendars.SYNC_EVENTS}!=0", null)
|
||||
.map { LocalCalendar(it) }
|
||||
|
||||
override fun update(provider: ContentProviderClient, localCollection: LocalCalendar, fromCollection: Collection) {
|
||||
override fun update(client: ContentProviderClient, localCollection: LocalCalendar, fromCollection: Collection) {
|
||||
val accountSettings = accountSettingsFactory.create(localCollection.androidCalendar.account)
|
||||
val values = valuesFromCollectionInfo(fromCollection, withColor = accountSettings.getManageCalendarColors())
|
||||
|
||||
logger.log(Level.FINE, "Updating local calendar ${fromCollection.url}", values)
|
||||
localCollection.androidCalendar.update(values)
|
||||
val androidCalendar = localCollection.androidCalendar
|
||||
val provider = AndroidCalendarProvider(androidCalendar.account, client)
|
||||
provider.updateCalendar(androidCalendar.id, values)
|
||||
}
|
||||
|
||||
private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues {
|
||||
val values = ContentValues()
|
||||
values.put(Calendars._SYNC_ID, info.id)
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME,
|
||||
if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName)
|
||||
val values = contentValuesOf(
|
||||
Calendars._SYNC_ID to info.id,
|
||||
Calendars.CALENDAR_DISPLAY_NAME to
|
||||
if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName,
|
||||
|
||||
Calendars.ALLOWED_AVAILABILITY to arrayOf(
|
||||
Events.AVAILABILITY_BUSY,
|
||||
Events.AVAILABILITY_FREE
|
||||
).joinToString(",") { it.toString() },
|
||||
|
||||
Calendars.ALLOWED_ATTENDEE_TYPES to arrayOf(
|
||||
Attendees.TYPE_NONE,
|
||||
Attendees.TYPE_OPTIONAL,
|
||||
Attendees.TYPE_REQUIRED,
|
||||
Attendees.TYPE_RESOURCE
|
||||
).joinToString(",") { it.toString() },
|
||||
|
||||
Calendars.ALLOWED_REMINDERS to arrayOf(
|
||||
Reminders.METHOD_DEFAULT,
|
||||
Reminders.METHOD_ALERT,
|
||||
Reminders.METHOD_EMAIL
|
||||
).joinToString(",") { it.toString() },
|
||||
)
|
||||
|
||||
if (withColor && info.color != null)
|
||||
values.put(Calendars.CALENDAR_COLOR, info.color)
|
||||
@@ -105,9 +128,6 @@ class LocalCalendarStore @Inject constructor(
|
||||
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId))
|
||||
}
|
||||
|
||||
// add base values for Calendars
|
||||
values.putAll(calendarBaseValues)
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ interface LocalDataStore<T: LocalCollection<*>> {
|
||||
*
|
||||
* @return the new local collection, or `null` if creation failed
|
||||
*/
|
||||
fun create(provider: ContentProviderClient, fromCollection: Collection): T?
|
||||
fun create(client: ContentProviderClient, fromCollection: Collection): T?
|
||||
|
||||
/**
|
||||
* Returns all local collections of the data store, including those which don't have a corresponding remote
|
||||
@@ -50,7 +50,7 @@ interface LocalDataStore<T: LocalCollection<*>> {
|
||||
*
|
||||
* @return a list of all local collections
|
||||
*/
|
||||
fun getAll(account: Account, provider: ContentProviderClient): List<T>
|
||||
fun getAll(account: Account, client: ContentProviderClient): List<T>
|
||||
|
||||
/**
|
||||
* Updates the local collection with the data from the given (remote) collection info.
|
||||
@@ -59,7 +59,7 @@ interface LocalDataStore<T: LocalCollection<*>> {
|
||||
* @param localCollection the local collection to update
|
||||
* @param fromCollection collection info
|
||||
*/
|
||||
fun update(provider: ContentProviderClient, localCollection: T, fromCollection: Collection)
|
||||
fun update(client: ContentProviderClient, localCollection: T, fromCollection: Collection)
|
||||
|
||||
/**
|
||||
* Deletes the local collection.
|
||||
|
||||
@@ -8,10 +8,11 @@ import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Reminders
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.contentValuesOf
|
||||
import at.bitfire.davdroid.resource.LocalTask
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.techbee.jtx.JtxContract.asSyncAdapter
|
||||
import dagger.Binds
|
||||
@@ -45,8 +46,14 @@ class AccountSettingsMigration10 @Inject constructor(
|
||||
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED)
|
||||
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { provider ->
|
||||
provider.update(
|
||||
CalendarContract.Calendars.CONTENT_URI.asSyncAdapter(account),
|
||||
AndroidCalendar.calendarBaseValues, null, null)
|
||||
Calendars.CONTENT_URI.asSyncAdapter(account),
|
||||
contentValuesOf(
|
||||
Calendars.ALLOWED_REMINDERS to arrayOf(
|
||||
Reminders.METHOD_DEFAULT,
|
||||
Reminders.METHOD_ALERT,
|
||||
Reminders.METHOD_EMAIL
|
||||
).joinToString(",") { it.toString() }
|
||||
), null, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ package at.bitfire.davdroid.settings.migration
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract
|
||||
import androidx.annotation.OpenForTesting
|
||||
import androidx.core.content.contentValuesOf
|
||||
import at.bitfire.davdroid.db.Service
|
||||
@@ -18,6 +18,7 @@ import at.bitfire.davdroid.resource.LocalCalendarStore
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.davdroid.sync.TasksAppManager
|
||||
import at.bitfire.ical4android.JtxCollection
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
|
||||
import at.techbee.jtx.JtxContract
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
@@ -85,19 +86,18 @@ class AccountSettingsMigration20 @Inject constructor(
|
||||
} catch (_: SecurityException) {
|
||||
// no contacts permission
|
||||
null
|
||||
}?.use { provider ->
|
||||
for (calendar in calendarStore.getAll(account, provider))
|
||||
provider.query(calendar.androidCalendar.calendarSyncURI(), arrayOf(Calendars.NAME), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst())
|
||||
cursor.getString(0)?.let { url ->
|
||||
collectionRepository.getByServiceAndUrl(calDavServiceId, url)?.let { collection ->
|
||||
calendar.androidCalendar.update(
|
||||
contentValuesOf(
|
||||
Calendars._SYNC_ID to collection.id
|
||||
))
|
||||
}
|
||||
}
|
||||
}?.use { client ->
|
||||
val calendarProvider = AndroidCalendarProvider(account, client)
|
||||
// for each calendar, assign _SYNC_ID := ID if collection (identified by NAME field = URL)
|
||||
for (calendar in calendarProvider.findCalendars()) {
|
||||
val url = calendar.name ?: continue
|
||||
collectionRepository.getByServiceAndUrl(calDavServiceId, url)?.let { collection ->
|
||||
calendar.update(contentValuesOf(
|
||||
CalendarContract.Calendars._SYNC_ID to collection.id
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.sync.account.setAndVerifyUserData
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -19,7 +19,6 @@ import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Inject
|
||||
import kotlin.use
|
||||
|
||||
class AccountSettingsMigration7 @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
@@ -27,8 +26,9 @@ class AccountSettingsMigration7 @Inject constructor(
|
||||
|
||||
override fun migrate(account: Account) {
|
||||
// add calendar colors
|
||||
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { provider ->
|
||||
AndroidCalendar.insertColors(provider, account)
|
||||
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { client ->
|
||||
val provider = AndroidCalendarProvider(account, client)
|
||||
provider.provideCss3ColorIndices()
|
||||
}
|
||||
|
||||
// update allowed WiFi settings key
|
||||
|
||||
@@ -11,7 +11,7 @@ import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.resource.LocalCalendarStore
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
@@ -43,10 +43,12 @@ class CalendarSyncer @AssistedInject constructor(
|
||||
override fun prepare(provider: ContentProviderClient): Boolean {
|
||||
// Update colors
|
||||
val accountSettings = accountSettingsFactory.create(account)
|
||||
|
||||
val calendarProvider = AndroidCalendarProvider(account, provider)
|
||||
if (accountSettings.getEventColors())
|
||||
AndroidCalendar.insertColors(provider, account)
|
||||
calendarProvider.provideCss3ColorIndices()
|
||||
else
|
||||
AndroidCalendar.removeColors(provider, account)
|
||||
calendarProvider.removeColorIndices()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,11 @@ import androidx.lifecycle.ViewModel
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.repository.DavCollectionRepository
|
||||
import at.bitfire.davdroid.repository.DavHomeSetRepository
|
||||
import at.bitfire.ical4android.Css3Color
|
||||
import at.bitfire.synctools.icalendar.Css3Color
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.text.Collator
|
||||
import java.time.ZoneId
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -28,6 +24,10 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.Collator
|
||||
import java.time.ZoneId
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
@HiltViewModel(assistedFactory = CreateCalendarModel.Factory::class)
|
||||
class CreateCalendarModel @AssistedInject constructor(
|
||||
|
||||
@@ -62,7 +62,7 @@ import at.bitfire.davdroid.ui.AppTheme
|
||||
import at.bitfire.davdroid.ui.composable.ExceptionInfoDialog
|
||||
import at.bitfire.davdroid.ui.composable.ProgressBar
|
||||
import at.bitfire.davdroid.ui.widget.CalendarColorPickerDialog
|
||||
import at.bitfire.ical4android.Css3Color
|
||||
import at.bitfire.synctools.icalendar.Css3Color
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -25,7 +25,7 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import at.bitfire.ical4android.Css3Color
|
||||
import at.bitfire.synctools.icalendar.Css3Color
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class PlainTextFormatterTest {
|
||||
|
||||
private val minimum = PlainTextFormatter(
|
||||
withTime = false,
|
||||
withSource = false,
|
||||
withException = false,
|
||||
lineSeparator = null
|
||||
)
|
||||
|
||||
@Test
|
||||
fun test_format_param_null() {
|
||||
val result = minimum.format(LogRecord(Level.INFO, "Message").apply {
|
||||
parameters = arrayOf(null)
|
||||
})
|
||||
assertEquals("Message\n\tPARAMETER #1 = (null)", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_format_param_object() {
|
||||
val result = minimum.format(LogRecord(Level.INFO, "Message").apply {
|
||||
parameters = arrayOf(object {
|
||||
override fun toString() = "SomeObject[]"
|
||||
})
|
||||
})
|
||||
assertEquals("Message\n\tPARAMETER #1 = SomeObject[]", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_format_truncatesMessage() {
|
||||
val result = minimum.format(LogRecord(Level.INFO, "a".repeat(50000)))
|
||||
// PlainTextFormatter.MAX_LENGTH is 10,000
|
||||
assertEquals(10000, result.length)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@ androidx-test-junit = "1.2.1"
|
||||
androidx-work = "2.10.2"
|
||||
bitfire-cert4android = "b67ba86d31"
|
||||
bitfire-dav4jvm = "acbfbacbaf"
|
||||
bitfire-synctools = "7bd154b5be"
|
||||
bitfire-synctools = "a365b91c04"
|
||||
compose-accompanist = "0.37.3"
|
||||
compose-bom = "2025.06.01"
|
||||
dnsjava = "3.6.3"
|
||||
|
||||
Reference in New Issue
Block a user