diff --git a/CONTRIBUTING_A_NEW_QUEST.md b/CONTRIBUTING_A_NEW_QUEST.md
index 4a5f1df189..c735ab4219 100644
--- a/CONTRIBUTING_A_NEW_QUEST.md
+++ b/CONTRIBUTING_A_NEW_QUEST.md
@@ -380,7 +380,7 @@ See "logcat" (bottom left area of the screen) to see stacktrace or logging messa
## Adding logs
```kotlin
-import android.util.Log
+import de.westnordost.streetcomplete.util.logs.Log
Log.w("Unique string for easy grepping in logcat", "Message with whatever you need like #${someVariable.itsProperty}")
```
diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/logs/LogsDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/logs/LogsDaoTest.kt
new file mode 100644
index 0000000000..81f1b41868
--- /dev/null
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/logs/LogsDaoTest.kt
@@ -0,0 +1,80 @@
+package de.westnordost.streetcomplete.data.logs
+
+import de.westnordost.streetcomplete.data.ApplicationDbTestCase
+import de.westnordost.streetcomplete.data.logs.LogLevel.*
+import de.westnordost.streetcomplete.util.ktx.containsExactlyInAnyOrder
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class LogsDaoTest : ApplicationDbTestCase() {
+ private lateinit var dao: LogsDao
+
+ @BeforeTest fun createDao() {
+ dao = LogsDao(database)
+ }
+
+ @Test fun getAll_sorts_by_timestamp_descending() {
+ val m1 = createMessage("1", timestamp = 1)
+ val m2 = createMessage("2", timestamp = 100)
+ val m3 = createMessage("3", timestamp = 10)
+
+ listOf(m1, m2, m3).forEach { dao.add(it) }
+
+ // sorted by timestamp ascending
+ assertEquals(listOf(m1, m3, m2), dao.getAll())
+ }
+
+ @Test fun getAll_filters_by_levels() {
+ val m1 = createMessage("1", level = VERBOSE)
+ val m2 = createMessage("2", level = WARNING)
+ val m3 = createMessage("3", level = ERROR)
+
+ listOf(m1, m2, m3).forEach { dao.add(it) }
+
+ assertTrue(dao.getAll(levels = setOf(WARNING, ERROR)).containsExactlyInAnyOrder(listOf(m2, m3)))
+ }
+
+ @Test fun getAll_filters_containing_string() {
+ val m1 = createMessage("foo")
+ val m2 = createMessage("bar")
+ val m3 = createMessage("foobar")
+
+ listOf(m1, m2, m3).forEach { dao.add(it) }
+
+ assertTrue(dao.getAll(messageContains = "foo").containsExactlyInAnyOrder(listOf(m1, m3)))
+ }
+
+ @Test fun getAll_filters_older_than_timestamp() {
+ val m1 = createMessage("1", timestamp = 1)
+ val m2 = createMessage("2", timestamp = 10)
+
+ listOf(m1, m2).forEach { dao.add(it) }
+
+ assertEquals(listOf(m1), dao.getAll(olderThan = 10))
+ }
+
+ @Test fun getAll_filters_newer_than_timestamp() {
+ val m1 = createMessage("1", timestamp = 1)
+ val m2 = createMessage("2", timestamp = 10)
+
+ listOf(m1, m2).forEach { dao.add(it) }
+
+ assertEquals(listOf(m2), dao.getAll(newerThan = 1))
+ }
+}
+
+private fun createMessage(
+ message: String,
+ level: LogLevel = VERBOSE,
+ timestamp: Long = 1
+) = LogMessage(
+ level,
+ TAG,
+ message,
+ null,
+ timestamp
+)
+
+private const val TAG = "LogsDaoTest"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/ApplicationConstants.kt b/app/src/main/java/de/westnordost/streetcomplete/ApplicationConstants.kt
index 677f2748e2..c5abbc0be2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/ApplicationConstants.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/ApplicationConstants.kt
@@ -27,6 +27,12 @@ object ApplicationConstants {
* database if not used anymore and have not been refreshed in the meantime */
const val DELETE_OLD_DATA_AFTER = 14L * 24 * 60 * 60 * 1000 // 14 days in ms
+ /** the duration after which logs will be deleted from the database */
+ const val DELETE_OLD_LOG_AFTER = 14L * 24 * 60 * 60 * 1000 // 14 days in ms
+
+ /** the duration after which logs won't be attached to the crash report */
+ const val DO_NOT_ATTACH_LOG_TO_CRASH_REPORT_AFTER = 5L * 60 * 1000 // 5 minutes in ms
+
const val NOTE_MIN_ZOOM = 15
/** when new quests that are appearing due to download of an area, show the hint that he can
diff --git a/app/src/main/java/de/westnordost/streetcomplete/ApplicationModule.kt b/app/src/main/java/de/westnordost/streetcomplete/ApplicationModule.kt
index 4f16b00b8b..af2fc35579 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/ApplicationModule.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/ApplicationModule.kt
@@ -6,6 +6,7 @@ import android.content.res.Resources
import androidx.preference.PreferenceManager
import de.westnordost.streetcomplete.util.CrashReportExceptionHandler
import de.westnordost.streetcomplete.util.SoundFx
+import de.westnordost.streetcomplete.util.logs.DatabaseLogger
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
@@ -14,6 +15,7 @@ val appModule = module {
factory { androidContext().resources }
factory { PreferenceManager.getDefaultSharedPreferences(androidContext()) }
- single { CrashReportExceptionHandler(androidContext(), "streetcomplete_errors@westnordost.de", "crashreport.txt") }
+ single { CrashReportExceptionHandler(androidContext(), get(), "streetcomplete_errors@westnordost.de", "crashreport.txt") }
+ single { DatabaseLogger(get()) }
single { SoundFx(androidContext()) }
}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt b/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt
index 5ea8e74f7c..4229b01bd6 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt
@@ -16,6 +16,7 @@ import de.westnordost.streetcomplete.data.download.downloadModule
import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesController
import de.westnordost.streetcomplete.data.edithistory.EditHistoryController
import de.westnordost.streetcomplete.data.edithistory.editHistoryModule
+import de.westnordost.streetcomplete.data.logs.logsModule
import de.westnordost.streetcomplete.data.maptiles.maptilesModule
import de.westnordost.streetcomplete.data.messages.messagesModule
import de.westnordost.streetcomplete.data.meta.metadataModule
@@ -51,6 +52,9 @@ import de.westnordost.streetcomplete.util.getSelectedLocale
import de.westnordost.streetcomplete.util.getSystemLocales
import de.westnordost.streetcomplete.util.ktx.addedToFront
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.AndroidLogger
+import de.westnordost.streetcomplete.util.logs.DatabaseLogger
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.setDefaultLocales
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -66,6 +70,7 @@ import java.util.concurrent.TimeUnit
class StreetCompleteApplication : Application() {
private val preloader: Preloader by inject()
+ private val databaseLogger: DatabaseLogger by inject()
private val crashReportExceptionHandler: CrashReportExceptionHandler by inject()
private val resurveyIntervalsUpdater: ResurveyIntervalsUpdater by inject()
private val downloadedTilesController: DownloadedTilesController by inject()
@@ -89,6 +94,7 @@ class StreetCompleteApplication : Application() {
appModule,
createdElementsModule,
dbModule,
+ logsModule,
downloadModule,
editHistoryModule,
elementEditsModule,
@@ -119,6 +125,8 @@ class StreetCompleteApplication : Application() {
)
}
+ setLoggerInstances()
+
/* Force log out users who use the old OAuth consumer key+secret because it does not exist
anymore. Trying to use that does not result in a "not authorized" API response, but some
response the app cannot handle */
@@ -188,6 +196,11 @@ class StreetCompleteApplication : Application() {
AppCompatDelegate.setDefaultNightMode(theme.appCompatNightMode)
}
+ private fun setLoggerInstances() {
+ Log.instances.add(AndroidLogger())
+ Log.instances.add(databaseLogger)
+ }
+
private fun enqueuePeriodicCleanupWork() {
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"Cleanup",
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt b/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt
index 808d6d8ea2..f29d4f136f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt
@@ -1,20 +1,22 @@
package de.westnordost.streetcomplete.data
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesController
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataController
import de.westnordost.streetcomplete.data.osmnotes.NoteController
import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry
+import de.westnordost.streetcomplete.screens.about.LogsController
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
/** Deletes old unused data in the background */
class Cleaner(
private val noteController: NoteController,
private val mapDataController: MapDataController,
private val questTypeRegistry: QuestTypeRegistry,
- private val downloadedTilesController: DownloadedTilesController
+ private val downloadedTilesController: DownloadedTilesController,
+ private val logsController: LogsController
) {
fun clean() {
val time = nowAsEpochMilliseconds()
@@ -26,6 +28,9 @@ class Cleaner(
/* do this after cleaning map data and notes, because some metadata rely on map data */
questTypeRegistry.forEach { it.deleteMetadataOlderThan(oldDataTimestamp) }
+ val oldLogTimestamp = nowAsEpochMilliseconds() - ApplicationConstants.DELETE_OLD_LOG_AFTER
+ logsController.deleteOlderThan(oldLogTimestamp)
+
Log.i(TAG, "Cleaning took ${((nowAsEpochMilliseconds() - time) / 1000.0).format(1)}s")
}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/OsmApiModule.kt b/app/src/main/java/de/westnordost/streetcomplete/data/OsmApiModule.kt
index 435691f8fa..57c07a88f9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/OsmApiModule.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/OsmApiModule.kt
@@ -16,7 +16,7 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module
val osmApiModule = module {
- factory { Cleaner(get(), get(), get(), get()) }
+ factory { Cleaner(get(), get(), get(), get(), get()) }
factory { CacheTrimmer(get(), get()) }
factory { MapDataApiImpl(get()) }
factory { NotesApiImpl(get()) }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt
index 85ffe2e5b1..d1191518eb 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt
@@ -1,10 +1,10 @@
package de.westnordost.streetcomplete.data
-import android.util.Log
import de.westnordost.countryboundaries.CountryBoundaries
import de.westnordost.osmfeatures.FeatureDictionary
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt b/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt
index cd4e1e2258..6ca4b29ec8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt
@@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.content.contentValuesOf
import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesTable
+import de.westnordost.streetcomplete.data.logs.LogsTable
import de.westnordost.streetcomplete.data.osm.created_elements.CreatedElementsTable
import de.westnordost.streetcomplete.data.osm.edits.EditElementsTable
import de.westnordost.streetcomplete.data.osm.edits.ElementEditsTable
@@ -100,6 +101,10 @@ class StreetCompleteSQLiteOpenHelper(context: Context, dbName: String) :
// quest specific tables
db.execSQL(WayTrafficFlowTable.CREATE)
+
+ // logs
+ db.execSQL(LogsTable.CREATE)
+ db.execSQL(LogsTable.INDEX_CREATE)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -212,10 +217,14 @@ class StreetCompleteSQLiteOpenHelper(context: Context, dbName: String) :
db.execSQL(ElementIdProviderTable.ELEMENT_INDEX_CREATE)
}
+ if (oldVersion <= 11 && newVersion > 11) {
+ db.execSQL(LogsTable.CREATE)
+ db.execSQL(LogsTable.INDEX_CREATE)
+ }
}
}
-private const val DB_VERSION = 11
+private const val DB_VERSION = 12
private fun SQLiteDatabase.renameQuest(old: String, new: String) {
renameValue(ElementEditsTable.NAME, ElementEditsTable.Columns.QUEST_TYPE, old, new)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/download/DownloadService.kt b/app/src/main/java/de/westnordost/streetcomplete/data/download/DownloadService.kt
index 32e33b7650..942debeb8a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/download/DownloadService.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/download/DownloadService.kt
@@ -6,13 +6,12 @@ import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants.NOTIFICATIONS_ID_SYNC
import de.westnordost.streetcomplete.data.download.tiles.TilesRect
import de.westnordost.streetcomplete.data.sync.CoroutineIntentService
import de.westnordost.streetcomplete.data.sync.createSyncNotification
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.CancellationException
-import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.koin.android.ext.android.inject
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt
index de4a92539f..00f50a4566 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.download
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesController
import de.westnordost.streetcomplete.data.download.tiles.TilesRect
@@ -9,6 +8,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataDownloader
import de.westnordost.streetcomplete.data.osmnotes.NotesDownloader
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.math.area
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt b/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt
index fb5b353334..ffa492bdc1 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.download.strategy
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesSource
import de.westnordost.streetcomplete.data.download.tiles.TilesRect
@@ -10,6 +9,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataController
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.math.area
import de.westnordost.streetcomplete.util.math.enclosingBoundingBox
import kotlinx.coroutines.Dispatchers
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogLevel.kt b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogLevel.kt
new file mode 100644
index 0000000000..cf436c8bb5
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogLevel.kt
@@ -0,0 +1,28 @@
+package de.westnordost.streetcomplete.data.logs
+
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.logs.LogLevel.*
+
+enum class LogLevel {
+ VERBOSE,
+ DEBUG,
+ INFO,
+ WARNING,
+ ERROR
+}
+
+val LogLevel.styleResId: Int get() = when (this) {
+ VERBOSE -> R.style.TextAppearance_LogMessage_Verbose
+ DEBUG -> R.style.TextAppearance_LogMessage_Debug
+ INFO -> R.style.TextAppearance_LogMessage_Info
+ WARNING -> R.style.TextAppearance_LogMessage_Warning
+ ERROR -> R.style.TextAppearance_LogMessage_Error
+}
+
+val LogLevel.colorId: Int get() = when (this) {
+ VERBOSE -> R.color.log_verbose
+ DEBUG -> R.color.log_debug
+ INFO -> R.color.log_info
+ WARNING -> R.color.log_warning
+ ERROR -> R.color.log_error
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogMessage.kt b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogMessage.kt
new file mode 100644
index 0000000000..299906538c
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogMessage.kt
@@ -0,0 +1,33 @@
+package de.westnordost.streetcomplete.data.logs
+
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+
+data class LogMessage(
+ val level: LogLevel,
+ val tag: String,
+ val message: String,
+ val error: String?,
+ val timestamp: Long
+) {
+ override fun toString(): String {
+ var string = "[$tag] $message"
+
+ if (error != null) {
+ string += " $error"
+ }
+
+ return string
+ }
+}
+
+fun Iterable.format(tz: TimeZone = TimeZone.currentSystemDefault()): String {
+ return joinToString("\n") {
+ val timestamp = Instant.fromEpochMilliseconds(it.timestamp)
+ .toLocalDateTime(tz)
+ .toString()
+
+ "$timestamp: $it"
+ }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsDao.kt
new file mode 100644
index 0000000000..a415bf53ae
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsDao.kt
@@ -0,0 +1,68 @@
+package de.westnordost.streetcomplete.data.logs
+
+import de.westnordost.streetcomplete.data.CursorPosition
+import de.westnordost.streetcomplete.data.Database
+import de.westnordost.streetcomplete.data.logs.LogsTable.Columns.ERROR
+import de.westnordost.streetcomplete.data.logs.LogsTable.Columns.LEVEL
+import de.westnordost.streetcomplete.data.logs.LogsTable.Columns.MESSAGE
+import de.westnordost.streetcomplete.data.logs.LogsTable.Columns.TAG
+import de.westnordost.streetcomplete.data.logs.LogsTable.Columns.TIMESTAMP
+import de.westnordost.streetcomplete.data.logs.LogsTable.NAME
+
+/** Stores the app logs */
+class LogsDao(private val db: Database) {
+ fun getAll(
+ levels: Set = LogLevel.values().toSet(),
+ messageContains: String? = null,
+ newerThan: Long? = null,
+ olderThan: Long? = null,
+ ): List {
+ val levelsString = levels.joinToString(",") { "'${it.name}'" }
+ var where = "$LEVEL IN ($levelsString)"
+ var args = arrayOf()
+
+ if (messageContains != null) {
+ where += " AND $MESSAGE LIKE ?"
+ args += "%$messageContains%"
+ }
+
+ if (newerThan != null) {
+ where += " AND $TIMESTAMP > ?"
+ args += newerThan
+ }
+
+ if (olderThan != null) {
+ where += " AND $TIMESTAMP < ?"
+ args += olderThan
+ }
+
+ return db.query(
+ NAME,
+ where = where,
+ args = args,
+ orderBy = "$TIMESTAMP ASC"
+ ) { it.toLogMessage() }
+ }
+
+ fun add(message: LogMessage) {
+ db.insert(NAME, message.toPairs())
+ }
+
+ fun deleteOlderThan(time: Long): Int = db.delete(NAME, where = "$TIMESTAMP < $time")
+}
+
+private fun LogMessage.toPairs(): List> = listOfNotNull(
+ LEVEL to level.name,
+ TAG to tag,
+ MESSAGE to message,
+ ERROR to error,
+ TIMESTAMP to timestamp
+)
+
+private fun CursorPosition.toLogMessage() = LogMessage(
+ LogLevel.valueOf(getString(LEVEL)),
+ getString(TAG),
+ getString(MESSAGE),
+ getStringOrNull(ERROR),
+ getLong(TIMESTAMP)
+)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsModule.kt b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsModule.kt
new file mode 100644
index 0000000000..b270261639
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsModule.kt
@@ -0,0 +1,10 @@
+package de.westnordost.streetcomplete.data.logs
+
+import de.westnordost.streetcomplete.screens.about.LogsController
+import org.koin.dsl.module
+
+val logsModule = module {
+ factory { LogsDao(get()) }
+
+ single { LogsController(get()) }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsTable.kt b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsTable.kt
new file mode 100644
index 0000000000..678b4df250
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/logs/LogsTable.kt
@@ -0,0 +1,27 @@
+package de.westnordost.streetcomplete.data.logs
+
+object LogsTable {
+ const val NAME = "logs"
+
+ object Columns {
+ const val LEVEL = "level"
+ const val TAG = "tag"
+ const val MESSAGE = "message"
+ const val ERROR = "error"
+ const val TIMESTAMP = "timestamp"
+ }
+
+ const val CREATE = """
+ CREATE TABLE $NAME (
+ ${Columns.LEVEL} text NOT NULL,
+ ${Columns.TAG} text NOT NULL,
+ ${Columns.MESSAGE} text NOT NULL,
+ ${Columns.ERROR} text,
+ ${Columns.TIMESTAMP} int NOT NULL
+ );
+ """
+
+ const val INDEX_CREATE = """
+ CREATE INDEX logs_timestamp_index ON $NAME (${Columns.TIMESTAMP});
+ """
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt
index e6141a7a16..a75e362200 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt
@@ -1,12 +1,12 @@
package de.westnordost.streetcomplete.data.maptiles
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect
import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
import de.westnordost.streetcomplete.screens.main.map.VectorTileProvider
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/ElementEditsUploader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/ElementEditsUploader.kt
index de7475b791..d87c73acb5 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/ElementEditsUploader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/ElementEditsUploader.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.osm.edits.upload
-import android.util.Log
import de.westnordost.streetcomplete.data.osm.edits.ElementEdit
import de.westnordost.streetcomplete.data.osm.edits.ElementEditsController
import de.westnordost.streetcomplete.data.osm.edits.ElementIdProvider
@@ -17,6 +16,7 @@ import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsController
import de.westnordost.streetcomplete.data.upload.ConflictException
import de.westnordost.streetcomplete.data.upload.OnUploadedChangeListener
import de.westnordost.streetcomplete.data.user.statistics.StatisticsController
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt
index 91e1bd2abb..13f9d7127a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.osm.edits.upload.changesets
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants.QUESTTYPE_TAG_KEY
import de.westnordost.streetcomplete.ApplicationConstants.USER_AGENT
import de.westnordost.streetcomplete.data.osm.edits.ElementEditType
@@ -8,6 +7,7 @@ import de.westnordost.streetcomplete.data.osm.edits.upload.LastEditTimeStore
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataApi
import de.westnordost.streetcomplete.data.upload.ConflictException
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
import java.util.Locale
/** Manages the creation and reusage of changesets */
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt
index d233df7e65..cb73351406 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.osm.mapdata
-import android.util.Log
import de.westnordost.streetcomplete.data.osm.created_elements.CreatedElementsController
import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometryCreator
@@ -9,6 +8,7 @@ import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometryEntry
import de.westnordost.streetcomplete.util.Listeners
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
/** Controller to access element data and its geometry and handle updates to it (from OSM API) */
class MapDataController internal constructor(
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt
index 349f67b395..533f0d659e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt
@@ -1,10 +1,10 @@
package de.westnordost.streetcomplete.data.osm.mapdata
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.download.QueryTooBigException
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.math.enlargedBy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt
index 5feec11cad..3c94d2cb83 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt
@@ -13,7 +13,6 @@ import de.westnordost.streetcomplete.data.osm.mapdata.RelationTables.Columns.VER
import de.westnordost.streetcomplete.data.osm.mapdata.RelationTables.NAME
import de.westnordost.streetcomplete.data.osm.mapdata.RelationTables.NAME_MEMBERS
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
-import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt
index 0083d2e381..672e4dff36 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.osm.osmquests
-import android.util.Log
import de.westnordost.countryboundaries.CountryBoundaries
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource
@@ -28,6 +27,7 @@ import de.westnordost.streetcomplete.util.ktx.intersects
import de.westnordost.streetcomplete.util.ktx.isInAny
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
import de.westnordost.streetcomplete.util.ktx.truncateTo5Decimals
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.math.contains
import de.westnordost.streetcomplete.util.math.enclosingBoundingBox
import de.westnordost.streetcomplete.util.math.enlargedBy
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt
index 9bfec14c05..52d16c022a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt
@@ -1,10 +1,10 @@
package de.westnordost.streetcomplete.data.osmnotes
-import android.util.Log
import de.westnordost.osmapi.user.UserApi
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
import de.westnordost.streetcomplete.util.ktx.saveToFile
+import de.westnordost.streetcomplete.util.logs.Log
import java.io.File
import java.io.IOException
import java.net.URL
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt
index 23aa5da277..73907afd99 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt
@@ -1,11 +1,11 @@
package de.westnordost.streetcomplete.data.osmnotes
-import android.util.Log
import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.util.Listeners
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
/** Manages access to the notes storage */
class NoteController(
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt
index 768c5e77cb..a87f1db0d1 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt
@@ -14,7 +14,6 @@ import de.westnordost.streetcomplete.data.osmnotes.NoteTable.Columns.LONGITUDE
import de.westnordost.streetcomplete.data.osmnotes.NoteTable.Columns.STATUS
import de.westnordost.streetcomplete.data.osmnotes.NoteTable.NAME
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
-import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt
index 6bf52fbfba..2ef54e6962 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt
@@ -1,9 +1,9 @@
package de.westnordost.streetcomplete.data.osmnotes
-import android.util.Log
import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt
index 3e4689ffbe..82f0d81ed3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.osmnotes.edits
-import android.util.Log
import de.westnordost.streetcomplete.data.osmnotes.NoteController
import de.westnordost.streetcomplete.data.osmnotes.NotesApi
import de.westnordost.streetcomplete.data.osmnotes.StreetCompleteImageUploader
@@ -13,6 +12,7 @@ import de.westnordost.streetcomplete.data.upload.ConflictException
import de.westnordost.streetcomplete.data.upload.OnUploadedChangeListener
import de.westnordost.streetcomplete.data.user.UserDataSource
import de.westnordost.streetcomplete.util.ktx.truncate
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestAutoSyncer.kt b/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestAutoSyncer.kt
index e58a0ddd2f..73e2e17cad 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestAutoSyncer.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestAutoSyncer.kt
@@ -7,7 +7,6 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.net.ConnectivityManager
-import android.util.Log
import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@@ -26,6 +25,7 @@ import de.westnordost.streetcomplete.data.visiblequests.TeamModeQuestFilter
import de.westnordost.streetcomplete.util.ktx.format
import de.westnordost.streetcomplete.util.ktx.toLatLon
import de.westnordost.streetcomplete.util.location.FineLocationManager
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/upload/UploadService.kt b/app/src/main/java/de/westnordost/streetcomplete/data/upload/UploadService.kt
index 3b29c03e81..b182e062fa 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/upload/UploadService.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/upload/UploadService.kt
@@ -6,11 +6,11 @@ import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants.NOTIFICATIONS_ID_SYNC
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.data.sync.CoroutineIntentService
import de.westnordost.streetcomplete.data.sync.createSyncNotification
+import de.westnordost.streetcomplete.util.logs.Log
import org.koin.android.ext.android.inject
/** Collects and uploads all changes the user has done: notes he left, comments he left on existing
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/upload/Uploader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/upload/Uploader.kt
index 8d838f1f12..d1d4065cbc 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/upload/Uploader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/upload/Uploader.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.data.upload
-import android.util.Log
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesController
import de.westnordost.streetcomplete.data.download.tiles.enclosingTilePos
@@ -9,6 +8,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsUploader
import de.westnordost.streetcomplete.data.user.AuthorizationException
import de.westnordost.streetcomplete.data.user.UserLoginStatusSource
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/UserUpdater.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/UserUpdater.kt
index 27eaee8ffc..eded776845 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/UserUpdater.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/UserUpdater.kt
@@ -1,11 +1,11 @@
package de.westnordost.streetcomplete.data.user
-import android.util.Log
import de.westnordost.osmapi.user.UserApi
import de.westnordost.streetcomplete.data.osmnotes.AvatarsDownloader
import de.westnordost.streetcomplete.data.user.statistics.StatisticsController
import de.westnordost.streetcomplete.data.user.statistics.StatisticsDownloader
import de.westnordost.streetcomplete.util.Listeners
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
index ce80d898f8..32f64f0470 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
@@ -1,7 +1,6 @@
package de.westnordost.streetcomplete.data.user.statistics
import android.content.SharedPreferences
-import android.util.Log
import androidx.core.content.edit
import de.westnordost.countryboundaries.CountryBoundaries
import de.westnordost.streetcomplete.Prefs
@@ -12,6 +11,7 @@ import de.westnordost.streetcomplete.util.ktx.getIds
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
import de.westnordost.streetcomplete.util.ktx.systemTimeNow
import de.westnordost.streetcomplete.util.ktx.toLocalDate
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import java.util.concurrent.FutureTask
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/AttachPhotoFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/AttachPhotoFragment.kt
index 466abbdd65..fc6daaa0c2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/AttachPhotoFragment.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/AttachPhotoFragment.kt
@@ -6,7 +6,6 @@ import android.content.pm.PackageManager.FEATURE_CAMERA_ANY
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Environment
-import android.util.Log
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
@@ -22,6 +21,7 @@ import de.westnordost.streetcomplete.databinding.FragmentAttachPhotoBinding
import de.westnordost.streetcomplete.util.decodeScaledBitmapAndNormalize
import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
import de.westnordost.streetcomplete.util.ktx.toast
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.viewBinding
import de.westnordost.streetcomplete.view.AdapterDataChangedWatcher
import java.io.File
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt
index bd3e5f92d8..7edc837323 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOneway.kt
@@ -1,6 +1,5 @@
package de.westnordost.streetcomplete.quests.oneway_suspects
-import android.util.Log
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression
import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
@@ -14,6 +13,7 @@ import de.westnordost.streetcomplete.osm.Tags
import de.westnordost.streetcomplete.quests.oneway_suspects.data.TrafficFlowSegment
import de.westnordost.streetcomplete.quests.oneway_suspects.data.TrafficFlowSegmentsApi
import de.westnordost.streetcomplete.quests.oneway_suspects.data.WayTrafficFlowDao
+import de.westnordost.streetcomplete.util.logs.Log
import kotlin.math.hypot
class AddSuspectedOneway(
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsAdapter.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsAdapter.kt
new file mode 100644
index 0000000000..8de121b055
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsAdapter.kt
@@ -0,0 +1,56 @@
+package de.westnordost.streetcomplete.screens.about
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.widget.TextViewCompat
+import androidx.recyclerview.widget.RecyclerView
+import de.westnordost.streetcomplete.data.logs.LogMessage
+import de.westnordost.streetcomplete.data.logs.styleResId
+import de.westnordost.streetcomplete.databinding.RowLogMessageBinding
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+
+class LogsAdapter : RecyclerView.Adapter() {
+
+ class ViewHolder(private val binding: RowLogMessageBinding) : RecyclerView.ViewHolder(binding.root) {
+ fun onBind(with: LogMessage) {
+ binding.messageTextView.text = with.toString()
+
+ TextViewCompat.setTextAppearance(binding.messageTextView, with.level.styleResId)
+
+ binding.timestampTextView.text = Instant
+ .fromEpochMilliseconds(with.timestamp)
+ .toLocalDateTime(TimeZone.currentSystemDefault())
+ .time
+ .toString()
+ }
+ }
+
+ var messages: List
+ get() = _messages
+ set(value) {
+ _messages = value.toMutableList()
+ notifyDataSetChanged()
+ }
+
+ private var _messages: MutableList = mutableListOf()
+
+ fun add(message: LogMessage) {
+ _messages.add(message)
+ notifyItemInserted(_messages.lastIndex)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ val binding = RowLogMessageBinding.inflate(inflater, parent, false)
+
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.onBind(_messages[position])
+ }
+
+ override fun getItemCount() = _messages.size
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsController.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsController.kt
new file mode 100644
index 0000000000..af3165305c
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsController.kt
@@ -0,0 +1,59 @@
+package de.westnordost.streetcomplete.screens.about
+
+import de.westnordost.streetcomplete.data.logs.LogLevel
+import de.westnordost.streetcomplete.data.logs.LogMessage
+import de.westnordost.streetcomplete.data.logs.LogsDao
+import de.westnordost.streetcomplete.util.logs.Log
+import java.util.concurrent.CopyOnWriteArrayList
+
+class LogsController(private val logsDao: LogsDao) {
+
+ /** Interface to be notified of new log messages */
+ interface Listener {
+ fun onAdded(message: LogMessage)
+ }
+
+ private val listeners: MutableList = CopyOnWriteArrayList()
+
+ fun getLogs(
+ levels: Set = LogLevel.values().toSet(),
+ messageContains: String? = null,
+ newerThan: Long? = null,
+ olderThan: Long? = null,
+ ): List {
+ return logsDao.getAll(
+ levels = levels,
+ messageContains = messageContains,
+ newerThan = newerThan,
+ olderThan = olderThan,
+ )
+ }
+
+ fun deleteOlderThan(timestamp: Long) {
+ val deletedCount = logsDao.deleteOlderThan(timestamp)
+ if (deletedCount > 0) {
+ Log.v(TAG, "Deleted $deletedCount old log messages")
+ }
+ }
+
+ fun add(message: LogMessage) {
+ logsDao.add(message)
+ onAdded(message)
+ }
+
+ fun addListener(listener: Listener) {
+ listeners.add(listener)
+ }
+
+ fun removeListener(listener: Listener) {
+ listeners.remove(listener)
+ }
+
+ private fun onAdded(message: LogMessage) {
+ listeners.forEach { it.onAdded(message) }
+ }
+
+ companion object {
+ private const val TAG = "LogsController"
+ }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsFiltersDialog.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsFiltersDialog.kt
new file mode 100644
index 0000000000..6e9e68c441
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsFiltersDialog.kt
@@ -0,0 +1,222 @@
+package de.westnordost.streetcomplete.screens.about
+
+import android.annotation.SuppressLint
+import android.app.DatePickerDialog
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.LayoutInflater
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import androidx.core.widget.TextViewCompat
+import androidx.core.widget.doAfterTextChanged
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipDrawable
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.logs.LogLevel
+import de.westnordost.streetcomplete.data.logs.LogMessage
+import de.westnordost.streetcomplete.data.logs.colorId
+import de.westnordost.streetcomplete.data.logs.styleResId
+import de.westnordost.streetcomplete.databinding.DialogLogsFiltersBinding
+import de.westnordost.streetcomplete.util.dateTimeToString
+import de.westnordost.streetcomplete.util.ktx.nonBlankTextOrNull
+import de.westnordost.streetcomplete.util.ktx.now
+import de.westnordost.streetcomplete.util.ktx.toEpochMilli
+import de.westnordost.streetcomplete.view.dialogs.TimePickerDialog
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.LocalTime
+import kotlinx.serialization.Serializable
+import java.util.Locale
+import kotlin.coroutines.resume
+
+class LogsFiltersDialog(
+ private val context: Context,
+ initialFilters: LogsFilters,
+ onApplyButtonClick: (filters: LogsFilters) -> Unit
+) : AlertDialog(context) {
+
+ private val filters = initialFilters.copy()
+ private val binding = DialogLogsFiltersBinding.inflate(LayoutInflater.from(context))
+ private val locale = Locale.getDefault()
+
+ init {
+ setView(binding.root)
+
+ setButton(BUTTON_POSITIVE, context.getString(R.string.action_filter)) { _, _ ->
+ onApplyButtonClick(filters)
+ dismiss()
+ }
+ setButton(BUTTON_NEGATIVE, context.getString(R.string.action_reset)) { _, _ ->
+ onApplyButtonClick(LogsFilters())
+ cancel()
+ }
+
+ createLogLevelsChips()
+
+ binding.messageContainsEditText.setText(filters.messageContains)
+ binding.messageContainsEditText.doAfterTextChanged {
+ filters.messageContains = binding.messageContainsEditText.nonBlankTextOrNull
+ }
+
+ updateNewerThanInput()
+ binding.newerThanEditText.setOnClickListener {
+ lifecycleScope.launch {
+ filters.timestampNewerThan = pickDateTime(
+ filters.timestampNewerThan ?: LocalDateTime.now()
+ )
+ updateNewerThanInput()
+ }
+ }
+ binding.newerThanEditTextLayout.setEndIconOnClickListener {
+ filters.timestampNewerThan = null
+ updateNewerThanInput()
+ }
+
+ updateOlderThanInput()
+ binding.olderThanEditText.setOnClickListener {
+ lifecycleScope.launch {
+ filters.timestampOlderThan = pickDateTime(
+ filters.timestampOlderThan ?: LocalDateTime.now()
+ )
+ updateOlderThanInput()
+ }
+ }
+ binding.olderThanEditTextLayout.setEndIconOnClickListener {
+ filters.timestampOlderThan = null
+ updateOlderThanInput()
+ }
+ }
+
+ private fun createLogLevelsChips() {
+ LogLevel.values().forEach { level ->
+ val chip = LogLevelFilterChip(level, context)
+
+ chip.isChecked = filters.levels.contains(level)
+ chip.isChipIconVisible = !chip.isChecked
+
+ chip.setOnClickListener {
+ chip.isChipIconVisible = !chip.isChecked
+
+ when (filters.levels.contains(level)) {
+ true -> filters.levels.remove(level)
+ false -> filters.levels.add(level)
+ }
+ }
+
+ binding.levelChipGroup.addView(chip)
+ }
+ }
+
+ private fun updateNewerThanInput() {
+ binding.newerThanEditTextLayout.isEndIconVisible = (filters.timestampNewerThan != null)
+ binding.newerThanEditText.setText(filters.timestampNewerThan?.let { dateTimeToString(locale, it) } ?: "")
+ }
+
+ private fun updateOlderThanInput() {
+ binding.olderThanEditTextLayout.isEndIconVisible = (filters.timestampOlderThan != null)
+ binding.olderThanEditText.setText(filters.timestampOlderThan?.let { dateTimeToString(locale, it) } ?: "")
+ }
+
+ private suspend fun pickDateTime(initialDateTime: LocalDateTime): LocalDateTime {
+ val date = pickDate(initialDateTime.date)
+ val time = pickTime(initialDateTime.time)
+
+ return LocalDateTime(date, time)
+ }
+
+ private suspend fun pickDate(initialDate: LocalDate): LocalDate =
+ suspendCancellableCoroutine { cont ->
+ DatePickerDialog(
+ context,
+ R.style.Theme_Bubble_Dialog_DatePicker,
+ { _, year, month, dayOfMonth ->
+ cont.resume(LocalDate(year, month, dayOfMonth))
+ },
+ initialDate.year,
+ initialDate.monthNumber,
+ initialDate.dayOfMonth
+ ).show()
+ }
+
+ private suspend fun pickTime(initialTime: LocalTime): LocalTime =
+ suspendCancellableCoroutine { cont ->
+ TimePickerDialog(
+ context,
+ initialTime.hour,
+ initialTime.minute,
+ true
+ ) { hour, minute ->
+ cont.resume(LocalTime(hour, minute))
+ }.show()
+ }
+}
+
+@SuppressLint("ViewConstructor")
+class LogLevelFilterChip(level: LogLevel, context: Context) : Chip(context) {
+ init {
+ val drawable = ChipDrawable.createFromAttributes(
+ context,
+ null,
+ 0,
+ com.google.android.material.R.style.Widget_MaterialComponents_Chip_Filter
+ )
+
+ setChipDrawable(drawable)
+
+ setCheckedIconResource(R.drawable.ic_check_circle_24dp)
+ checkedIconTint = ColorStateList.valueOf(ContextCompat.getColor(context, level.colorId))
+
+ setChipIconResource(R.drawable.ic_circle_outline_24dp)
+ chipIconTint = ColorStateList.valueOf(ContextCompat.getColor(context, level.colorId))
+
+ text = level.name
+ TextViewCompat.setTextAppearance(this, level.styleResId)
+ }
+}
+
+@Serializable
+data class LogsFilters(
+ var levels: MutableSet = LogLevel.values().toMutableSet(),
+ var messageContains: String? = null,
+ var timestampNewerThan: LocalDateTime? = null,
+ var timestampOlderThan: LocalDateTime? = null
+) {
+ fun copy(): LogsFilters = LogsFilters(
+ levels.toMutableSet(),
+ messageContains,
+ timestampNewerThan,
+ timestampOlderThan
+ )
+
+ fun matches(message: LogMessage): Boolean {
+ if (!levels.contains(message.level)) {
+ return false
+ }
+
+ if (
+ messageContains != null &&
+ !message.message.contains(messageContains!!, ignoreCase = true)
+ ) {
+ return false
+ }
+
+ if (
+ timestampNewerThan != null &&
+ message.timestamp <= timestampNewerThan!!.toEpochMilli()
+ ) {
+ return false
+ }
+
+ if (
+ timestampOlderThan != null &&
+ message.timestamp >= timestampOlderThan!!.toEpochMilli()
+ ) {
+ return false
+ }
+
+ return true
+ }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsFragment.kt
new file mode 100644
index 0000000000..28570ce093
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsFragment.kt
@@ -0,0 +1,145 @@
+package de.westnordost.streetcomplete.screens.about
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.widget.Toolbar
+import androidx.recyclerview.widget.DividerItemDecoration
+import de.westnordost.streetcomplete.BuildConfig
+import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.logs.LogMessage
+import de.westnordost.streetcomplete.data.logs.format
+import de.westnordost.streetcomplete.databinding.FragmentLogsBinding
+import de.westnordost.streetcomplete.screens.TwoPaneDetailFragment
+import de.westnordost.streetcomplete.util.ktx.now
+import de.westnordost.streetcomplete.util.ktx.toEpochMilli
+import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
+import de.westnordost.streetcomplete.util.viewBinding
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.datetime.LocalDateTime
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import org.koin.android.ext.android.inject
+
+/** Shows the app logs */
+class LogsFragment : TwoPaneDetailFragment(R.layout.fragment_logs) {
+
+ private val logsController: LogsController by inject()
+ private val binding by viewBinding(FragmentLogsBinding::bind)
+ private val adapter = LogsAdapter()
+
+ private var filters = LogsFilters()
+
+ private val logsControllerListener = object : LogsController.Listener {
+ override fun onAdded(message: LogMessage) { viewLifecycleScope.launch { onMessageAdded(message) } }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ createOptionsMenu(binding.toolbar.root)
+
+ binding.logsList.adapter = adapter
+ binding.logsList.itemAnimator = null // default animations are too slow when logging many messages quickly
+ binding.logsList.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
+
+ if (savedInstanceState != null) {
+ onLoadInstanceState(savedInstanceState)
+ }
+
+ showLogs()
+
+ logsController.addListener(logsControllerListener)
+ }
+
+ private fun onLoadInstanceState(savedInstanceState: Bundle) {
+ filters = Json.decodeFromString(savedInstanceState.getString(FILTERS_DATA)!!)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(FILTERS_DATA, Json.encodeToString(filters))
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ logsController.removeListener(logsControllerListener)
+ }
+
+ private fun createOptionsMenu(toolbar: Toolbar) {
+ toolbar.inflateMenu(R.menu.menu_logs)
+
+ toolbar.setOnMenuItemClickListener { item ->
+ when (item.itemId) {
+ R.id.action_share -> {
+ onClickShare()
+ true
+ }
+ R.id.action_filter -> {
+ onClickFilter()
+ true
+ }
+ else -> false
+ }
+ }
+ }
+
+ private fun onClickShare() = viewLifecycleScope.launch {
+ val shareIntent = Intent(Intent.ACTION_SEND).apply {
+ val logText = getLogs().format()
+ val logTimestamp = LocalDateTime.now().toString()
+ val logTitle =
+ "${BuildConfig.APPLICATION_ID}_${BuildConfig.VERSION_NAME}_$logTimestamp.log"
+
+ putExtra(Intent.EXTRA_TEXT, logText)
+ putExtra(Intent.EXTRA_TITLE, logTitle)
+ type = "text/plain"
+ }
+
+ startActivity(Intent.createChooser(shareIntent, null))
+ }
+
+ private fun onClickFilter() {
+ LogsFiltersDialog(requireContext(), filters) { newFilters ->
+ filters = newFilters
+ showLogs()
+ }.show()
+ }
+
+ private fun onMessageAdded(message: LogMessage) {
+ if (filters.matches(message)) {
+ adapter.add(message)
+ binding.toolbar.root.title = getString(R.string.about_title_logs, adapter.messages.size)
+
+ if (hasScrolledToBottom()) {
+ binding.logsList.scrollToPosition(adapter.messages.lastIndex)
+ }
+ }
+ }
+
+ private fun showLogs() {
+ viewLifecycleScope.launch {
+ val logs = getLogs()
+ adapter.messages = logs
+ binding.toolbar.root.title = getString(R.string.about_title_logs, logs.size)
+ binding.logsList.scrollToPosition(logs.lastIndex)
+ }
+ }
+
+ private suspend fun getLogs(): List = withContext(Dispatchers.IO) {
+ logsController.getLogs(
+ levels = filters.levels,
+ messageContains = filters.messageContains,
+ newerThan = filters.timestampNewerThan?.toEpochMilli(),
+ olderThan = filters.timestampOlderThan?.toEpochMilli()
+ )
+ }
+
+ private fun hasScrolledToBottom(): Boolean = !binding.logsList.canScrollVertically(1)
+
+ companion object {
+ private const val FILTERS_DATA = "filters_data"
+ }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/MarkerManager.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/MarkerManager.kt
index 2b9d9dc133..b4d0299551 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/MarkerManager.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/MarkerManager.kt
@@ -1,12 +1,12 @@
package de.westnordost.streetcomplete.screens.main.map.tangram
import android.graphics.drawable.BitmapDrawable
-import android.util.Log
import com.mapzen.tangram.LngLat
import com.mapzen.tangram.MapController
import com.mapzen.tangram.geometry.Polygon
import com.mapzen.tangram.geometry.Polyline
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
+import de.westnordost.streetcomplete.util.logs.Log
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.coroutines.Continuation
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/OAuthFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/OAuthFragment.kt
index 931f398617..d67c1a540c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/OAuthFragment.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/OAuthFragment.kt
@@ -3,7 +3,6 @@ package de.westnordost.streetcomplete.screens.user.login
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Bundle
-import android.util.Log
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
@@ -17,6 +16,7 @@ import de.westnordost.streetcomplete.databinding.FragmentOauthBinding
import de.westnordost.streetcomplete.screens.HasTitle
import de.westnordost.streetcomplete.util.ktx.toast
import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.viewBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/CrashReportExceptionHandler.kt b/app/src/main/java/de/westnordost/streetcomplete/util/CrashReportExceptionHandler.kt
index 22fd7aa35f..2a0bb09c3c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/util/CrashReportExceptionHandler.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/CrashReportExceptionHandler.kt
@@ -5,10 +5,19 @@ import android.content.Context
import android.os.Build
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.BuildConfig
import de.westnordost.streetcomplete.R
+import de.westnordost.streetcomplete.data.logs.format
+import de.westnordost.streetcomplete.screens.about.LogsController
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
import de.westnordost.streetcomplete.util.ktx.sendEmail
import de.westnordost.streetcomplete.util.ktx.toast
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
@@ -20,6 +29,7 @@ import java.util.Locale
* on next startup */
class CrashReportExceptionHandler(
private val appCtx: Context,
+ private val logsController: LogsController,
private val mailReportTo: String,
private val crashReportFile: String
) : Thread.UncaughtExceptionHandler {
@@ -47,26 +57,21 @@ class CrashReportExceptionHandler(
}
}
- fun askUserToSendErrorReport(activityCtx: Activity, @StringRes titleResourceId: Int, e: Exception) {
- val stackTrace = StringWriter()
- e.printStackTrace(PrintWriter(stackTrace))
- askUserToSendErrorReport(activityCtx, titleResourceId, stackTrace.toString())
+ fun askUserToSendErrorReport(activityCtx: AppCompatActivity, @StringRes titleResourceId: Int, e: Exception) {
+ activityCtx.lifecycleScope.launch {
+ val reportText = withContext(Dispatchers.IO) { createErrorReport(e, null) }
+ askUserToSendErrorReport(activityCtx, titleResourceId, reportText)
+ }
}
- private fun askUserToSendErrorReport(activityCtx: Activity, @StringRes titleResourceId: Int, error: String?) {
- val report = """
- Describe how to reproduce it here:
-
-
-
- $error
- """.trimIndent()
+ private fun askUserToSendErrorReport(activityCtx: Activity, @StringRes titleResourceId: Int, reportText: String?) {
+ val mailText = "Describe how to reproduce it here:\n\n\n\n$reportText"
AlertDialog.Builder(activityCtx)
.setTitle(titleResourceId)
.setMessage(R.string.crash_message)
.setPositiveButton(R.string.crash_compose_email) { _, _ ->
- activityCtx.sendEmail(mailReportTo, "Error Report", report)
+ activityCtx.sendEmail(mailReportTo, "Error Report", mailText)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
activityCtx.toast("\uD83D\uDE22")
@@ -75,18 +80,40 @@ class CrashReportExceptionHandler(
.show()
}
- override fun uncaughtException(t: Thread, e: Throwable) {
+ override fun uncaughtException(thread: Thread, error: Throwable) {
+ val report = createErrorReport(error, thread)
+
+ writeCrashReportToFile(report)
+ defaultUncaughtExceptionHandler!!.uncaughtException(thread, error)
+ }
+
+ private fun createErrorReport(error: Throwable, thread: Thread?): String {
val stackTrace = StringWriter()
- e.printStackTrace(PrintWriter(stackTrace))
- writeCrashReportToFile("""
- Thread: ${t.name}
+ error.printStackTrace(PrintWriter(stackTrace))
+
+ val logText = readLogFromDatabase()
+
+ var report = ""
+
+ if (thread != null) {
+ report += "Thread: ${thread.name}"
+ }
+
+ report += """
App version: ${BuildConfig.VERSION_NAME}
Device: ${Build.BRAND} ${Build.DEVICE}, Android ${Build.VERSION.RELEASE}
Locale: ${Locale.getDefault()}
+
Stack trace:
- $stackTrace
- """.trimIndent())
- defaultUncaughtExceptionHandler!!.uncaughtException(t, e)
+
+ """.trimIndent()
+
+ report += stackTrace
+
+ report += "\nLog:\n"
+ report += logText
+
+ return report
}
private fun writeCrashReportToFile(text: String) {
@@ -109,4 +136,13 @@ class CrashReportExceptionHandler(
private fun deleteCrashReport() {
appCtx.deleteFile(crashReportFile)
}
+
+ private fun readLogFromDatabase(): String {
+ val newLogTimestamp =
+ nowAsEpochMilliseconds() - ApplicationConstants.DO_NOT_ATTACH_LOG_TO_CRASH_REPORT_AFTER
+
+ return logsController
+ .getLogs(newerThan = newLogTimestamp)
+ .format()
+ }
}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt b/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt
index 1562854252..726331e34a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt
@@ -6,7 +6,10 @@ import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
+import kotlinx.datetime.toJavaLocalDateTime
import java.text.DateFormat
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
import java.util.Locale
fun timeOfDayToString(locale: Locale, minutes: Int): String {
@@ -16,3 +19,11 @@ fun timeOfDayToString(locale: Locale, minutes: Int): String {
.toEpochMilliseconds()
return DateFormat.getTimeInstance(DateFormat.SHORT, locale).format(todayAt)
}
+
+fun dateTimeToString(locale: Locale, dateTime: LocalDateTime): String {
+ val formatter = DateTimeFormatter
+ .ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT)
+ .withLocale(locale)
+
+ return dateTime.toJavaLocalDateTime().format(formatter)
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/SpatialCache.kt b/app/src/main/java/de/westnordost/streetcomplete/util/SpatialCache.kt
index a690405a53..73d448439c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/util/SpatialCache.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/SpatialCache.kt
@@ -1,12 +1,12 @@
package de.westnordost.streetcomplete.util
-import android.util.Log
import de.westnordost.streetcomplete.data.download.tiles.TilePos
import de.westnordost.streetcomplete.data.download.tiles.enclosingTilePos
import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect
import de.westnordost.streetcomplete.data.download.tiles.minTileRect
import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
+import de.westnordost.streetcomplete.util.logs.Log
import de.westnordost.streetcomplete.util.math.contains
import de.westnordost.streetcomplete.util.math.isCompletelyInside
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/ktx/LocalDateTime.kt b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/LocalDateTime.kt
new file mode 100644
index 0000000000..c2d295ddbc
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/LocalDateTime.kt
@@ -0,0 +1,8 @@
+package de.westnordost.streetcomplete.util.ktx
+
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toInstant
+
+fun LocalDateTime.toEpochMilli(): Long =
+ this.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/logs/AndroidLogger.kt b/app/src/main/java/de/westnordost/streetcomplete/util/logs/AndroidLogger.kt
new file mode 100644
index 0000000000..05344eabf7
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/logs/AndroidLogger.kt
@@ -0,0 +1,25 @@
+package de.westnordost.streetcomplete.util.logs
+
+import android.util.Log
+
+class AndroidLogger() : Logger {
+ override fun v(tag: String, message: String) {
+ Log.v(tag, message)
+ }
+
+ override fun d(tag: String, message: String) {
+ Log.d(tag, message)
+ }
+
+ override fun i(tag: String, message: String) {
+ Log.i(tag, message)
+ }
+
+ override fun w(tag: String, message: String, exception: Throwable?) {
+ if (exception != null) Log.w(tag, message, exception) else Log.w(tag, message)
+ }
+
+ override fun e(tag: String, message: String, exception: Throwable?) {
+ if (exception != null) Log.e(tag, message, exception) else Log.e(tag, message)
+ }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/logs/DatabaseLogger.kt b/app/src/main/java/de/westnordost/streetcomplete/util/logs/DatabaseLogger.kt
new file mode 100644
index 0000000000..1eb7bc41ba
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/logs/DatabaseLogger.kt
@@ -0,0 +1,48 @@
+package de.westnordost.streetcomplete.util.logs
+
+import de.westnordost.streetcomplete.data.logs.LogLevel
+import de.westnordost.streetcomplete.data.logs.LogLevel.*
+import de.westnordost.streetcomplete.data.logs.LogMessage
+import de.westnordost.streetcomplete.screens.about.LogsController
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+
+class DatabaseLogger(private val logsController: LogsController) : Logger {
+ private val coroutineScope = CoroutineScope(SupervisorJob() + CoroutineName("DatabaseLogger") + Dispatchers.IO)
+
+ override fun v(tag: String, message: String) {
+ log(VERBOSE, tag, message)
+ }
+
+ override fun d(tag: String, message: String) {
+ log(DEBUG, tag, message)
+ }
+
+ override fun i(tag: String, message: String) {
+ log(INFO, tag, message)
+ }
+
+ override fun w(tag: String, message: String, exception: Throwable?) {
+ log(WARNING, tag, message, exception)
+ }
+
+ override fun e(tag: String, message: String, exception: Throwable?) {
+ log(ERROR, tag, message, exception)
+ }
+
+ private fun log(level: LogLevel, tag: String, message: String, exception: Throwable? = null) {
+ coroutineScope.launch {
+ logsController.add(LogMessage(
+ level = level,
+ tag = tag,
+ message = message,
+ error = exception?.toString(),
+ timestamp = nowAsEpochMilliseconds()
+ ))
+ }
+ }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/logs/Log.kt b/app/src/main/java/de/westnordost/streetcomplete/util/logs/Log.kt
new file mode 100644
index 0000000000..c501386572
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/logs/Log.kt
@@ -0,0 +1,35 @@
+package de.westnordost.streetcomplete.util.logs
+
+object Log : Logger {
+ var instances: MutableList = mutableListOf()
+
+ override fun v(tag: String, message: String) {
+ instances.forEach {
+ it.v(tag, message)
+ }
+ }
+
+ override fun d(tag: String, message: String) {
+ instances.forEach {
+ it.d(tag, message)
+ }
+ }
+
+ override fun i(tag: String, message: String) {
+ instances.forEach {
+ it.i(tag, message)
+ }
+ }
+
+ override fun w(tag: String, message: String, exception: Throwable?) {
+ instances.forEach {
+ it.w(tag, message, exception)
+ }
+ }
+
+ override fun e(tag: String, message: String, exception: Throwable?) {
+ instances.forEach {
+ it.e(tag, message, exception)
+ }
+ }
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/logs/Logger.kt b/app/src/main/java/de/westnordost/streetcomplete/util/logs/Logger.kt
new file mode 100644
index 0000000000..56c5b0dd55
--- /dev/null
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/logs/Logger.kt
@@ -0,0 +1,18 @@
+package de.westnordost.streetcomplete.util.logs
+
+interface Logger {
+ /** Send VERBOSE log [message] */
+ fun v(tag: String, message: String)
+
+ /** Send DEBUG log [message] */
+ fun d(tag: String, message: String)
+
+ /** Send INFO log [message] */
+ fun i(tag: String, message: String)
+
+ /** Send WARNING log [message] with optional [exception] */
+ fun w(tag: String, message: String, exception: Throwable? = null)
+
+ /** Send ERROR log [message] with optional [exception] */
+ fun e(tag: String, message: String, exception: Throwable? = null)
+}
diff --git a/app/src/main/res/drawable/ic_calendar_month_24dp.xml b/app/src/main/res/drawable/ic_calendar_month_24dp.xml
new file mode 100644
index 0000000000..2e3f4c20cf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_calendar_month_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_cancel_24dp.xml b/app/src/main/res/drawable/ic_cancel_24dp.xml
new file mode 100644
index 0000000000..e9d4ba561f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cancel_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_check_circle_24dp.xml b/app/src/main/res/drawable/ic_check_circle_24dp.xml
new file mode 100644
index 0000000000..7f80c9f98f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check_circle_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_circle_outline_24dp.xml b/app/src/main/res/drawable/ic_circle_outline_24dp.xml
new file mode 100644
index 0000000000..4f5202cbbd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_circle_outline_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_filter_list_24dp.xml b/app/src/main/res/drawable/ic_filter_list_24dp.xml
new file mode 100644
index 0000000000..a488111f32
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filter_list_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_logs_filters.xml b/app/src/main/res/layout/dialog_logs_filters.xml
new file mode 100644
index 0000000000..f74932d815
--- /dev/null
+++ b/app/src/main/res/layout/dialog_logs_filters.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_logs.xml b/app/src/main/res/layout/fragment_logs.xml
new file mode 100644
index 0000000000..e800692c96
--- /dev/null
+++ b/app/src/main/res/layout/fragment_logs.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/row_log_message.xml b/app/src/main/res/layout/row_log_message.xml
new file mode 100644
index 0000000000..0149f75f4a
--- /dev/null
+++ b/app/src/main/res/layout/row_log_message.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_logs.xml b/app/src/main/res/menu/menu_logs.xml
new file mode 100644
index 0000000000..5669429018
--- /dev/null
+++ b/app/src/main/res/menu/menu_logs.xml
@@ -0,0 +1,13 @@
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index a3b4ea2305..d145651950 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -21,6 +21,13 @@
#70808b
+
+ #999
+ #90caf9
+ #a5d6a7
+ #ffcc80
+ #ef9a9a
+
#44f
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 07b1c2d320..3d5c899fbe 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -79,6 +79,12 @@
#AA335D
#655555
+ #666
+ #2196f3
+ #4caf50
+ #ff9800
+ #f44336
+
#22f
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 02d97a95c6..d9ec9155ef 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1708,5 +1708,15 @@ Alternatively, you can leave a note (with a photo)."
Chicane
Curb extension + traffic island
No narrowed lane here
+ Logs (%1$d)
+ for use in error reports
+ Filter
+ Share logs
+ Show logs
+ Level
+ From
+ To
+ Message contains
+ Filter messages
diff --git a/app/src/main/res/values/textStyles.xml b/app/src/main/res/values/textStyles.xml
index 00ed191b86..3af99130fe 100644
--- a/app/src/main/res/values/textStyles.xml
+++ b/app/src/main/res/values/textStyles.xml
@@ -49,4 +49,30 @@
- @color/hint_text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 835c3f26cf..d428f6fed9 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -71,11 +71,21 @@
+
+
+
+
diff --git a/app/src/main/res/xml/about.xml b/app/src/main/res/xml/about.xml
index 2a7556b0d9..f0619fce2d 100644
--- a/app/src/main/res/xml/about.xml
+++ b/app/src/main/res/xml/about.xml
@@ -73,6 +73,13 @@
android:widgetLayout="@layout/widget_image_open_in_browser"
/>
+
+
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/logs/LogMessageTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/logs/LogMessageTest.kt
new file mode 100644
index 0000000000..8f9428780c
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/logs/LogMessageTest.kt
@@ -0,0 +1,43 @@
+package de.westnordost.streetcomplete.data.logs
+
+import kotlinx.datetime.TimeZone
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class LogMessageTest {
+ @Test fun `toString is as expected`() {
+ assertEquals(
+ "[TAG] test message error",
+ LogMessage(
+ LogLevel.ERROR,
+ "TAG",
+ "test message",
+ "error",
+ 1000
+ ).toString()
+ )
+ }
+
+ @Test fun `format is as expected`() {
+ val m1 = LogMessage(
+ LogLevel.ERROR,
+ "TAG",
+ "test message",
+ "error",
+ 1000 * 60 * 30
+ )
+ val m2 = LogMessage(
+ LogLevel.ERROR,
+ "TAG",
+ "test message",
+ "error",
+ 1000 * 60 * 60
+ )
+
+ assertEquals(
+ "1970-01-01T00:30: [TAG] test message error\n" +
+ "1970-01-01T01:00: [TAG] test message error",
+ listOf(m1, m2).format(tz = TimeZone.UTC)
+ )
+ }
+}