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) + ) + } +}