diff --git a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt index b0f9a1d149..40ec753750 100644 --- a/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt +++ b/app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt @@ -2,6 +2,7 @@ package org.appdevforall.localwebserver import android.database.sqlite.SQLiteDatabase import com.aayushatharva.brotli4j.decoder.BrotliInputStream +import com.itsaky.androidide.utils.DatabaseVersionResolver import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -75,22 +76,7 @@ class WebServer(private val config: ServerConfig) { fun logDatabaseLastChanged() { try { - val query = """ -SELECT changeTime, who -FROM LastChange -""" - val cursor = database.rawQuery(query, arrayOf()) - - try { - if (cursor.count > 0) { - cursor.moveToFirst() - log.debug("Database last change: {} {}.", cursor.getString(0), cursor.getString(1)) - } - - } finally { - cursor.close() - } - + log.debug("Database last change: {}.", DatabaseVersionResolver.resolveDatabaseVersion(database)) } catch (e: Exception) { log.error("Could not retrieve database last change info: {}", e.message) } diff --git a/common/src/androidTest/java/com/itsaky/androidide/utils/DatabaseVersionResolverTest.kt b/common/src/androidTest/java/com/itsaky/androidide/utils/DatabaseVersionResolverTest.kt new file mode 100644 index 0000000000..1ba7c2eab1 --- /dev/null +++ b/common/src/androidTest/java/com/itsaky/androidide/utils/DatabaseVersionResolverTest.kt @@ -0,0 +1,107 @@ +package com.itsaky.androidide.utils + +import android.database.sqlite.SQLiteDatabase +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DatabaseVersionResolverTest { + + private lateinit var db: SQLiteDatabase + + @Before + fun setUp() { + db = SQLiteDatabase.openOrCreateDatabase(":memory:", null) + } + + @After + fun tearDown() { + db.close() + } + + private fun createTable() { + db.execSQL( + "CREATE TABLE LastChange (" + + "documentationSet TEXT, " + + "changeTime TEXT, " + + "who TEXT)" + ) + } + + private fun insertRow(documentationSet: String, changeTime: String, who: String?) { + db.execSQL( + "INSERT INTO LastChange (documentationSet, changeTime, who) VALUES (?, ?, ?)", + arrayOf(documentationSet, changeTime, who), + ) + } + + @Test + fun returnsWholedbRow_whenPresent() { + createTable() + insertRow("wholedb", "2026-05-09 02:00:20", "hal") + insertRow("tooltips-ide", "2026-05-09 02:00:20", "hal") + + assertEquals( + "2026-05-09 02:00:20 hal", + DatabaseVersionResolver.resolveDatabaseVersion(db), + ) + } + + @Test + fun fallsBackToLatestRow_whenWholedbMissing() { + createTable() + insertRow("tooltips-ide", "2026-05-09 02:00:20", "hal") + insertRow("content-y", "2026-05-01 17:42:29", "hal") + insertRow("tooltips-java", "2026-05-09 01:58:37", "hal") + + assertEquals( + "2026-05-09 02:00:20 (tooltips-ide) hal", + DatabaseVersionResolver.resolveDatabaseVersion(db), + ) + } + + @Test + fun returnsVersionUnknown_whenTableEmpty() { + createTable() + + assertEquals( + DatabaseVersionResolver.VERSION_UNKNOWN, + DatabaseVersionResolver.resolveDatabaseVersion(db), + ) + } + + @Test + fun returnsVersionUnknown_whenTableMissing() { + // Intentionally do not create the LastChange table. + assertEquals( + DatabaseVersionResolver.VERSION_UNKNOWN, + DatabaseVersionResolver.resolveDatabaseVersion(db), + ) + } + + @Test + fun handlesNullWho_onWholedbRow() { + createTable() + insertRow("wholedb", "2026-05-09 02:00:20", null) + + assertEquals( + "2026-05-09 02:00:20", + DatabaseVersionResolver.resolveDatabaseVersion(db), + ) + } + + @Test + fun handlesNullWho_onFallbackRow() { + createTable() + insertRow("tooltips-ide", "2026-05-09 02:00:20", null) + + assertEquals( + "2026-05-09 02:00:20 (tooltips-ide)", + DatabaseVersionResolver.resolveDatabaseVersion(db), + ) + } +} diff --git a/common/src/main/java/com/itsaky/androidide/utils/DatabaseVersionResolver.kt b/common/src/main/java/com/itsaky/androidide/utils/DatabaseVersionResolver.kt new file mode 100644 index 0000000000..711905eadd --- /dev/null +++ b/common/src/main/java/com/itsaky/androidide/utils/DatabaseVersionResolver.kt @@ -0,0 +1,71 @@ +package com.itsaky.androidide.utils + +import android.database.sqlite.SQLiteDatabase +import android.util.Log + +object DatabaseVersionResolver { + + const val VERSION_UNKNOWN = "Version Unknown" + + private const val TAG = "DatabaseVersionResolver" + + private const val QUERY_WHOLEDB = """ + SELECT changeTime, who + FROM LastChange + WHERE documentationSet = 'wholedb' + LIMIT 1 + """ + + private const val QUERY_FALLBACK_LATEST = """ + SELECT changeTime, documentationSet, who + FROM LastChange + ORDER BY changeTime DESC + LIMIT 1 + """ + + fun resolveDatabaseVersion(db: SQLiteDatabase): String { + return try { + db.rawQuery(QUERY_WHOLEDB, arrayOf()).use { c -> + if (c.moveToFirst()) { + return formatVersion( + changeTime = c.getString(0), + who = c.getString(1), + ) + } + } + + db.rawQuery(QUERY_FALLBACK_LATEST, arrayOf()).use { c -> + if (c.moveToFirst()) { + val result = formatVersion( + changeTime = c.getString(0), + who = c.getString(2), + documentationSet = c.getString(1), + ) + Log.e( + TAG, + "Missing 'wholedb' record in LastChange table; falling back to $result", + ) + return result + } + } + + Log.e(TAG, "No versioning information available") + VERSION_UNKNOWN + } catch (e: Exception) { + Log.e(TAG, "No versioning information available", e) + VERSION_UNKNOWN + } + } + + private fun formatVersion( + changeTime: String?, + who: String?, + documentationSet: String? = null, + ): String { + val parts = mutableListOf() + if (!changeTime.isNullOrBlank()) parts += changeTime + if (!documentationSet.isNullOrBlank()) parts += "($documentationSet)" + if (!who.isNullOrBlank()) parts += who + return parts.joinToString(separator = " ") + } +} diff --git a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt index b902b131e6..497b2f0086 100644 --- a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt +++ b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt @@ -28,6 +28,7 @@ import android.view.MotionEvent import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat.getColor import com.itsaky.androidide.activities.editor.HelpActivity +import com.itsaky.androidide.utils.DatabaseVersionResolver import com.itsaky.androidide.utils.Environment import com.itsaky.androidide.utils.FeedbackManager import com.itsaky.androidide.utils.isSystemInDarkMode @@ -66,12 +67,6 @@ object TooltipManager { ORDER BY buttonNumberId """ - private const val QUERY_LAST_CHANGE = """ - SELECT changeTime, who - FROM LastChange - WHERE documentationSet = 'wholedb' - """ - suspend fun getTooltip(context: Context, category: String, tag: String): IDETooltipItem? { Log.d(TAG, "In getTooltip() for category='$category', tag='$tag'.") @@ -98,9 +93,10 @@ object TooltipManager { val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY) db.use { database -> - database.rawQuery(QUERY_LAST_CHANGE, arrayOf()).use { c -> - c.moveToFirst() - lastChange = "${c.getString(0)} ${c.getString(1)}" + try { + lastChange = DatabaseVersionResolver.resolveDatabaseVersion(database) + } catch (e: Exception) { + Log.e(TAG, "Version resolution failed: ${e.message}") } Log.d(TAG, "last change is '${lastChange}'.")