From ffd37904dc72651069a45bc1c38c1b2fb6c27233 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Fri, 27 Dec 2024 12:39:45 +0100 Subject: [PATCH 1/2] Added printing of debug info for AccountsCleanupWorker Signed-off-by: Arnau Mora Gras --- .../kotlin/at/bitfire/davdroid/TextTable.kt | 8 +- .../at/bitfire/davdroid/ui/DebugInfoModel.kt | 105 +++++++++++++----- 2 files changed, 84 insertions(+), 29 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt b/app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt index ef51de43b..ed6e3c5c1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt @@ -7,7 +7,7 @@ package at.bitfire.davdroid import java.util.Collections class TextTable( - vararg val headers: String + val headers: List ) { companion object { @@ -18,10 +18,12 @@ class TextTable( } + constructor(vararg headers: String): this(headers.toList()) + private val lines = mutableListOf>() - fun addLine(vararg values: Any?) { + fun addLine(values: List) { if (values.size != headers.size) throw IllegalArgumentException("Table line must have ${headers.size} column(s)") lines += values.map { @@ -29,6 +31,8 @@ class TextTable( }.toTypedArray() } + fun addLine(vararg values: Any?) = addLine(values.toList()) + override fun toString(): String { val sb = StringBuilder() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt index fdb39197e..f59e95dfa 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt @@ -33,6 +33,7 @@ import androidx.core.content.getSystemService import androidx.core.content.pm.PackageInfoCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkQuery import at.bitfire.dav4jvm.exception.DavException @@ -48,6 +49,7 @@ import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncFrameworkIntegration +import at.bitfire.davdroid.sync.account.AccountsCleanupWorker import at.bitfire.davdroid.sync.worker.BaseSyncWorker import at.bitfire.ical4android.TaskProvider import at.techbee.jtx.JtxContract @@ -395,6 +397,11 @@ class DebugInfoModel @AssistedInject constructor( dumpAddressBookAccount(account, accountManager, writer) } + // general-purpose sync workers + writer.append("\nSync workers:\n") + .append(dumpSyncWorkersInfo()) + .append("\n") + // database dump writer.append("\n\nDATABASE DUMP\n\n") db.dump(writer, arrayOf("webdav_document")) @@ -552,38 +559,82 @@ class DebugInfoModel @AssistedInject constructor( return table.toString() } + private fun syncWorkersInfoTable( + query: WorkQuery, + extraColumns: Map String>> = emptyMap() + ): String { + val columnNames = mutableListOf("Tags", "State", "Next run", "Retries", "Generation", "Periodicity") + for ((index, column) in extraColumns) { + val (columnName) = column + columnNames.add(index, columnName) + } + + val table = TextTable(columnNames) + val workInfos = WorkManager.getInstance(context).getWorkInfos(query).get() + for (workInfo in workInfos) { + val line = mutableListOf( + workInfo.tags.map { it.replace("\\bat\\.bitfire\\.davdroid\\.".toRegex(), ".") }, + "${workInfo.state} (${workInfo.stopReason})", + workInfo.nextScheduleTimeMillis.let { nextRun -> + when (nextRun) { + Long.MAX_VALUE -> "—" + else -> DateUtils.getRelativeTimeSpanString(nextRun) + } + }, + workInfo.runAttemptCount, + workInfo.generation, + workInfo.periodicityInfo?.let { periodicity -> + "every ${periodicity.repeatIntervalMillis/60000} min" + } ?: "not periodic" + ) + + for ((index, column) in extraColumns) { + val (_, transformer) = column + val value = transformer(workInfo) + line.add(index, value) + } + + table.addLine(line) + } + return table.toString() + } + /** * Gets sync workers info * Note: WorkManager does not return worker names when queried, so we create them and ask * whether they exist one by one */ - private fun dumpSyncWorkersInfo(account: Account): String { - val table = TextTable("Tags", "Authority", "State", "Next run", "Retries", "Generation", "Periodicity") - for (dataType in SyncDataType.entries) { - val tag = BaseSyncWorker.commonTag(account, dataType) - WorkManager.getInstance(context).getWorkInfos( - WorkQuery.Builder.fromTags(listOf(tag)).build() - ).get().forEach { workInfo -> - table.addLine( - workInfo.tags.map { it.replace("\\bat\\.bitfire\\.davdroid\\.".toRegex(), ".") }, - dataType, - "${workInfo.state} (${workInfo.stopReason})", - workInfo.nextScheduleTimeMillis.let { nextRun -> - when (nextRun) { - Long.MAX_VALUE -> "—" - else -> DateUtils.getRelativeTimeSpanString(nextRun) - } - }, - workInfo.runAttemptCount, - workInfo.generation, - workInfo.periodicityInfo?.let { periodicity -> - "every ${periodicity.repeatIntervalMillis/60000} min" - } ?: "not periodic" - ) - } - } - return table.toString() - } + private fun dumpSyncWorkersInfo(account: Account): String = + syncWorkersInfoTable( + WorkQuery.Builder.fromTags( + SyncDataType.entries.map { BaseSyncWorker.commonTag(account, it) } + ).build(), + mapOf( + 1 to ("Data Type" to { workInfo: WorkInfo -> + // See: BaseSyncWorker.commonTag + // "sync-$dataType ${account.type}/${account.name}" + workInfo.tags + // Search for the first tag that starts with sync- + .first { it.startsWith("sync-") } + // Get everything before the space (get rid of the account) + .substringBefore(' ') + // Remove the "sync-" prefix + .removePrefix("sync-") + }) + ) + ) + + /** + * Gets account-independent sync workers info + * Note: WorkManager does not return worker names when queried, so we create them and ask + * whether they exist one by one + */ + private fun dumpSyncWorkersInfo(): String = + syncWorkersInfoTable( + WorkQuery.Builder.fromUniqueWorkNames( + listOf(AccountsCleanupWorker.NAME) + ).build() + ) } From cd4315c7d8d8cc224b138bd98feb6561a623469e Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Fri, 27 Dec 2024 12:43:40 +0100 Subject: [PATCH 2/2] Improved docs Signed-off-by: Arnau Mora Gras --- .../at/bitfire/davdroid/ui/DebugInfoModel.kt | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt index f59e95dfa..4ccba6383 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt @@ -559,7 +559,26 @@ class DebugInfoModel @AssistedInject constructor( return table.toString() } - private fun syncWorkersInfoTable( + /** + * Generates a table to display worker statuses. + * + * By default, the table provides the following columns: + * Tags, State, Next run, Retries, Generation, Periodicity + * + * If more tables are desired, they can be added using [extraColumns]. + * + * The key of the map is the position of the column relative to the default ones, so for example, + * if a column is going to be added between "State" and "Next run", the index should be `2`. + * + * The value the map is a pair whose first element is the column name, and the second one is + * the generator of the value. + * The generator will be called on every worker info found after running the query, and should + * return a String that will be placed in the cell. + * + * @param query The query to use for fetching the workers. + * @param extraColumns Defaults to an empty map, pass extra columns to be added to the table. + */ + private fun workersInfoTable( query: WorkQuery, extraColumns: Map String>> = emptyMap() ): String { @@ -605,7 +624,7 @@ class DebugInfoModel @AssistedInject constructor( * whether they exist one by one */ private fun dumpSyncWorkersInfo(account: Account): String = - syncWorkersInfoTable( + workersInfoTable( WorkQuery.Builder.fromTags( SyncDataType.entries.map { BaseSyncWorker.commonTag(account, it) } ).build(), @@ -630,7 +649,7 @@ class DebugInfoModel @AssistedInject constructor( * whether they exist one by one */ private fun dumpSyncWorkersInfo(): String = - syncWorkersInfoTable( + workersInfoTable( WorkQuery.Builder.fromUniqueWorkNames( listOf(AccountsCleanupWorker.NAME) ).build()