From 9594be8fcf57aff89ea6bae73a1953ebd1b698d7 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 21 Feb 2022 12:39:07 -0500 Subject: [PATCH] Add a 'Recent' tab to Spinner. --- .../securesms/database/DatabaseMonitor.kt | 31 ++++++ .../securesms/database/QueryMonitor.kt | 10 ++ .../securesms/database/SQLiteDatabase.java | 13 +++ .../securesms/SpinnerApplicationContext.kt | 21 ++++ .../org/signal/core/util/tracing/Tracer.java | 2 +- spinner/lib/src/main/assets/browse.hbs | 6 +- spinner/lib/src/main/assets/css/main.css | 89 +++++++++++++++++ spinner/lib/src/main/assets/error.hbs | 2 +- spinner/lib/src/main/assets/head.hbs | 99 ------------------- spinner/lib/src/main/assets/js/main.js | 15 +++ spinner/lib/src/main/assets/overview.hbs | 6 +- spinner/lib/src/main/assets/partials/head.hbs | 14 +++ .../src/main/assets/{ => partials}/prefix.hbs | 1 + .../lib/src/main/assets/partials/suffix.hbs | 1 + spinner/lib/src/main/assets/query.hbs | 17 +++- spinner/lib/src/main/assets/recent.hbs | 50 ++++++++++ spinner/lib/src/main/assets/suffix.hbs | 17 ---- .../java/org/signal/spinner/DatabaseUtil.kt | 2 +- .../main/java/org/signal/spinner/Spinner.kt | 72 +++++++++++++- .../java/org/signal/spinner/SpinnerServer.kt | 67 ++++++++++++- 20 files changed, 404 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/DatabaseMonitor.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/QueryMonitor.kt create mode 100644 spinner/lib/src/main/assets/css/main.css delete mode 100644 spinner/lib/src/main/assets/head.hbs create mode 100644 spinner/lib/src/main/assets/js/main.js create mode 100644 spinner/lib/src/main/assets/partials/head.hbs rename spinner/lib/src/main/assets/{ => partials}/prefix.hbs (89%) create mode 100644 spinner/lib/src/main/assets/partials/suffix.hbs create mode 100644 spinner/lib/src/main/assets/recent.hbs delete mode 100644 spinner/lib/src/main/assets/suffix.hbs diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseMonitor.kt b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseMonitor.kt new file mode 100644 index 0000000000..79ffc1a908 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseMonitor.kt @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues + +object DatabaseMonitor { + private var queryMonitor: QueryMonitor? = null + + fun initialize(queryMonitor: QueryMonitor?) { + DatabaseMonitor.queryMonitor = queryMonitor + } + + @JvmStatic + fun onSql(sql: String, args: Array?) { + queryMonitor?.onSql(sql, args) + } + + @JvmStatic + fun onQuery(distinct: Boolean, table: String, projection: Array?, selection: String?, args: Array?, groupBy: String?, having: String?, orderBy: String?, limit: String?) { + queryMonitor?.onQuery(distinct, table, projection, selection, args, groupBy, having, orderBy, limit) + } + + @JvmStatic + fun onDelete(table: String, selection: String?, args: Array?) { + queryMonitor?.onDelete(table, selection, args) + } + + @JvmStatic + fun onUpdate(table: String, values: ContentValues, selection: String?, args: Array?) { + queryMonitor?.onUpdate(table, values, selection, args) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/QueryMonitor.kt b/app/src/main/java/org/thoughtcrime/securesms/database/QueryMonitor.kt new file mode 100644 index 0000000000..371ea9a907 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/QueryMonitor.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues + +interface QueryMonitor { + fun onSql(sql: String, args: Array?) + fun onQuery(distinct: Boolean, table: String, projection: Array?, selection: String?, args: Array?, groupBy: String?, having: String?, orderBy: String?, limit: String?) + fun onDelete(table: String, selection: String?, args: Array?) + fun onUpdate(table: String, values: ContentValues, selection: String?, args: Array?) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SQLiteDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SQLiteDatabase.java index 6fed200de8..b21193b1b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SQLiteDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SQLiteDatabase.java @@ -226,34 +226,42 @@ public class SQLiteDatabase { } public Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { + DatabaseMonitor.onQuery(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); return traceSql("query(9)", table, selection, false, () -> wrapped.query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)); } public Cursor queryWithFactory(net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory cursorFactory, boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { + DatabaseMonitor.onQuery(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); return traceSql("queryWithFactory()", table, selection, false, () -> wrapped.queryWithFactory(cursorFactory, distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)); } public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) { + DatabaseMonitor.onQuery(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, null); return traceSql("query(7)", table, selection, false, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy)); } public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { + DatabaseMonitor.onQuery(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); return traceSql("query(8)", table, selection, false, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)); } public Cursor rawQuery(String sql, String[] selectionArgs) { + DatabaseMonitor.onSql(sql, selectionArgs); return traceSql("rawQuery(2a)", sql, false, () -> wrapped.rawQuery(sql, selectionArgs)); } public Cursor rawQuery(String sql, Object[] args) { + DatabaseMonitor.onSql(sql, args); return traceSql("rawQuery(2b)", sql, false,() -> wrapped.rawQuery(sql, args)); } public Cursor rawQueryWithFactory(net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { + DatabaseMonitor.onSql(sql, selectionArgs); return traceSql("rawQueryWithFactory()", sql, false, () -> wrapped.rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable)); } public Cursor rawQuery(String sql, String[] selectionArgs, int initialRead, int maxRead) { + DatabaseMonitor.onSql(sql, selectionArgs); return traceSql("rawQuery(4)", sql, false, () -> rawQuery(sql, selectionArgs, initialRead, maxRead)); } @@ -278,10 +286,12 @@ public class SQLiteDatabase { } public int delete(String table, String whereClause, String[] whereArgs) { + DatabaseMonitor.onDelete(table, whereClause, whereArgs); return traceSql("delete()", table, whereClause, true, () -> wrapped.delete(table, whereClause, whereArgs)); } public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { + DatabaseMonitor.onUpdate(table, values, whereClause, whereArgs); return traceSql("update()", table, whereClause, true, () -> wrapped.update(table, values, whereClause, whereArgs)); } @@ -290,14 +300,17 @@ public class SQLiteDatabase { } public void execSQL(String sql) throws SQLException { + DatabaseMonitor.onSql(sql, null); traceSql("execSQL(1)", sql, true, () -> wrapped.execSQL(sql)); } public void rawExecSQL(String sql) { + DatabaseMonitor.onSql(sql, null); traceSql("rawExecSQL()", sql, true, () -> wrapped.rawExecSQL(sql)); } public void execSQL(String sql, Object[] bindArgs) throws SQLException { + DatabaseMonitor.onSql(sql, null); traceSql("execSQL(2)", sql, true, () -> wrapped.execSQL(sql, bindArgs)); } diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt index e32df22c94..e608462274 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt @@ -1,13 +1,16 @@ package org.thoughtcrime.securesms +import android.content.ContentValues import android.os.Build import leakcanary.LeakCanary import org.signal.spinner.Spinner +import org.thoughtcrime.securesms.database.DatabaseMonitor import org.thoughtcrime.securesms.database.JobDatabase import org.thoughtcrime.securesms.database.KeyValueDatabase import org.thoughtcrime.securesms.database.LocalMetricsDatabase import org.thoughtcrime.securesms.database.LogDatabase import org.thoughtcrime.securesms.database.MegaphoneDatabase +import org.thoughtcrime.securesms.database.QueryMonitor import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.util.AppSignatureUtil import shark.AndroidReferenceMatchers @@ -33,6 +36,24 @@ class SpinnerApplicationContext : ApplicationContext() { ) ) + DatabaseMonitor.initialize(object : QueryMonitor { + override fun onSql(sql: String, args: Array?) { + Spinner.onSql("signal", sql, args) + } + + override fun onQuery(distinct: Boolean, table: String, projection: Array?, selection: String?, args: Array?, groupBy: String?, having: String?, orderBy: String?, limit: String?) { + Spinner.onQuery("signal", distinct, table, projection, selection, args, groupBy, having, orderBy, limit) + } + + override fun onDelete(table: String, selection: String?, args: Array?) { + Spinner.onDelete("signal", table, selection, args) + } + + override fun onUpdate(table: String, values: ContentValues, selection: String?, args: Array?) { + Spinner.onUpdate("signal", table, values, selection, args) + } + }) + LeakCanary.config = LeakCanary.config.copy( referenceMatchers = AndroidReferenceMatchers.appDefaults + AndroidReferenceMatchers.ignoredInstanceField( diff --git a/core-util/src/main/java/org/signal/core/util/tracing/Tracer.java b/core-util/src/main/java/org/signal/core/util/tracing/Tracer.java index 44d80d4be2..aed2386b8d 100644 --- a/core-util/src/main/java/org/signal/core/util/tracing/Tracer.java +++ b/core-util/src/main/java/org/signal/core/util/tracing/Tracer.java @@ -45,7 +45,7 @@ import java.util.concurrent.atomic.AtomicInteger; * and we want to create as little overhead as possible. The idea being that it's ok if we don't, * for example, keep a perfect circular buffer size if it allows us to reduce overhead. The only * cost of screwing up would be dropping a trace packet or something, which, while sad, won't affect - * how the app functions. + * how the app functions */ public final class Tracer { diff --git a/spinner/lib/src/main/assets/browse.hbs b/spinner/lib/src/main/assets/browse.hbs index c8a9909dee..5602f57300 100644 --- a/spinner/lib/src/main/assets/browse.hbs +++ b/spinner/lib/src/main/assets/browse.hbs @@ -1,8 +1,8 @@ - {{> head title="Browse" }} + {{> partials/head title="Browse" }} - {{> prefix isBrowse=true}} + {{> partials/prefix isBrowse=true}}
@@ -70,6 +70,6 @@
- {{> suffix}} + {{> partials/suffix}} diff --git a/spinner/lib/src/main/assets/css/main.css b/spinner/lib/src/main/assets/css/main.css new file mode 100644 index 0000000000..ab11ca9f20 --- /dev/null +++ b/spinner/lib/src/main/assets/css/main.css @@ -0,0 +1,89 @@ +html, body { + font-family: 'Roboto Mono', monospace; + font-variant-ligatures: none; + font-size: 12px; + width: 100%; +} + +select, input { + font-family: 'Roboto Mono', monospace; + font-variant-ligatures: none; + font-size: 1rem; +} + +table, th, td { + border: 1px solid black; + font-size: 1rem; +} + +th, td { + padding: 8px; +} + +.query-input { + width: 100%; + height: 10rem; + margin-bottom: 8px; +} + +li.active { + font-weight: bold; +} + +ol.tabs { + margin: 16px 0px 8px 0px; + padding: 0px; + font-size: 0px; +} + +.tabs li { + list-style-type: none; + display: inline-block; + padding: 8px; + border-bottom: 1px solid black; + font-size: 1rem; +} + +.tabs li.active { + border: 1px solid black; + border-bottom: 0; +} + +.tabs a { + text-decoration: none; + color: black; +} + +.collapse-header { + cursor: pointer; +} + +.collapse-header:before { + content: "⯈ "; + font-size: 1rem; +} + +.collapse-header.active:before { + content: "⯆ "; + font-size: 1rem; +} + +h2.collapse-header, h2.collapse-header+div { + margin-left: 16px; +} + +.hidden { + display: none; +} + +table.device-info { + margin-bottom: 16px; +} + +table.device-info, table.device-info tr, table.device-info td { + border: 0; + padding: 2px; + font-size: 0.75rem; + font-style: italic; +} + diff --git a/spinner/lib/src/main/assets/error.hbs b/spinner/lib/src/main/assets/error.hbs index c21e71fe48..4045ca6080 100644 --- a/spinner/lib/src/main/assets/error.hbs +++ b/spinner/lib/src/main/assets/error.hbs @@ -1,5 +1,5 @@ - {{> head title="Error :(" }} + {{> partials/head title="Error :(" }} Hit an exception while trying to serve the page :(
diff --git a/spinner/lib/src/main/assets/head.hbs b/spinner/lib/src/main/assets/head.hbs deleted file mode 100644 index fe5e6ea2f3..0000000000 --- a/spinner/lib/src/main/assets/head.hbs +++ /dev/null @@ -1,99 +0,0 @@ - - Spinner - {{ title }} - - - - - - - - - - diff --git a/spinner/lib/src/main/assets/js/main.js b/spinner/lib/src/main/assets/js/main.js new file mode 100644 index 0000000000..91be6e1ac6 --- /dev/null +++ b/spinner/lib/src/main/assets/js/main.js @@ -0,0 +1,15 @@ +function init() { + document.querySelectorAll('.collapse-header').forEach(elem => { + elem.onclick = () => { + console.log('clicked'); + elem.classList.toggle('active'); + document.getElementById(elem.dataset.for).classList.toggle('hidden'); + } + }); + + document.querySelector('#database-selector').onchange = (e) => { + window.location.href = window.location.href.split('?')[0] + '?db=' + e.target.value; + } +} + +init(); diff --git a/spinner/lib/src/main/assets/overview.hbs b/spinner/lib/src/main/assets/overview.hbs index e33be9d581..6b572fa989 100644 --- a/spinner/lib/src/main/assets/overview.hbs +++ b/spinner/lib/src/main/assets/overview.hbs @@ -1,5 +1,5 @@ - {{> head title="Home" }} + {{> partials/head title="Home" }} + diff --git a/spinner/lib/src/main/assets/prefix.hbs b/spinner/lib/src/main/assets/partials/prefix.hbs similarity index 89% rename from spinner/lib/src/main/assets/prefix.hbs rename to spinner/lib/src/main/assets/partials/prefix.hbs index 21b9acb1e0..f9ffe17213 100644 --- a/spinner/lib/src/main/assets/prefix.hbs +++ b/spinner/lib/src/main/assets/partials/prefix.hbs @@ -28,4 +28,5 @@
  • Overview
  • Browse
  • Query
  • +
  • Recent
  • \ No newline at end of file diff --git a/spinner/lib/src/main/assets/partials/suffix.hbs b/spinner/lib/src/main/assets/partials/suffix.hbs new file mode 100644 index 0000000000..760014f745 --- /dev/null +++ b/spinner/lib/src/main/assets/partials/suffix.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spinner/lib/src/main/assets/query.hbs b/spinner/lib/src/main/assets/query.hbs index dbb37de0e9..77ee1343b9 100644 --- a/spinner/lib/src/main/assets/query.hbs +++ b/spinner/lib/src/main/assets/query.hbs @@ -1,8 +1,8 @@ - {{> head title="Query" }} + {{> partials/head title="Query" }} - {{> prefix isQuery=true}} + {{> partials/prefix isQuery=true}}
    @@ -11,6 +11,8 @@ or + or +
    @@ -38,6 +40,15 @@ No data. {{/if}} - {{> suffix}} + {{> partials/suffix}} + + + diff --git a/spinner/lib/src/main/assets/recent.hbs b/spinner/lib/src/main/assets/recent.hbs new file mode 100644 index 0000000000..02755a672a --- /dev/null +++ b/spinner/lib/src/main/assets/recent.hbs @@ -0,0 +1,50 @@ + + {{> partials/head title="Home" }} + + + + + {{> partials/prefix isRecent=true}} + + {{#if recentSql}} + + {{#each recentSql}} + + + + + + {{/each}} +
    + {{formattedTime}} + +
    + + + + +
    +
    {{query}}
    + {{else}} + No recent queries. + {{/if}} + + {{> partials/suffix }} + + + + diff --git a/spinner/lib/src/main/assets/suffix.hbs b/spinner/lib/src/main/assets/suffix.hbs deleted file mode 100644 index c9157f431f..0000000000 --- a/spinner/lib/src/main/assets/suffix.hbs +++ /dev/null @@ -1,17 +0,0 @@ - \ No newline at end of file diff --git a/spinner/lib/src/main/java/org/signal/spinner/DatabaseUtil.kt b/spinner/lib/src/main/java/org/signal/spinner/DatabaseUtil.kt index 97db8601cf..14f374b843 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/DatabaseUtil.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/DatabaseUtil.kt @@ -34,4 +34,4 @@ fun SupportSQLiteDatabase.getTableRowCount(table: String): Int { 0 } } -} \ No newline at end of file +} diff --git a/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt b/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt index dcc420653d..d69493f0ba 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt @@ -1,6 +1,8 @@ package org.signal.spinner +import android.content.ContentValues import android.content.Context +import android.database.sqlite.SQLiteQueryBuilder import androidx.sqlite.db.SupportSQLiteDatabase import org.signal.core.util.logging.Log import java.io.IOException @@ -11,14 +13,80 @@ import java.io.IOException object Spinner { val TAG: String = Log.tag(Spinner::class.java) + private lateinit var server: SpinnerServer + fun init(context: Context, deviceInfo: DeviceInfo, databases: Map) { try { - SpinnerServer(context, deviceInfo, databases).start() + server = SpinnerServer(context, deviceInfo, databases) + server.start() } catch (e: IOException) { - Log.w(TAG, "Spinner server hit IO exception! Restarting.", e) + Log.w(TAG, "Spinner server hit IO exception!", e) } } + fun onSql(dbName: String, sql: String, args: Array?) { + server.onSql(dbName, replaceQueryArgs(sql, args)) + } + + fun onQuery(dbName: String, distinct: Boolean, table: String, projection: Array?, selection: String?, args: Array?, groupBy: String?, having: String?, orderBy: String?, limit: String?) { + val queryString = SQLiteQueryBuilder.buildQueryString(distinct, table, projection, selection, groupBy, having, orderBy, limit) + server.onSql(dbName, replaceQueryArgs(queryString, args)) + } + + fun onDelete(dbName: String, table: String, selection: String?, args: Array?) { + var query = "DELETE FROM $table" + if (selection != null) { + query += " WHERE $selection" + query = replaceQueryArgs(query, args) + } + + server.onSql(dbName, query) + } + + fun onUpdate(dbName: String, table: String, values: ContentValues, selection: String?, args: Array?) { + val query = StringBuilder("UPDATE $table SET ") + + for (key in values.keySet()) { + query.append("$key = ${values.get(key)}, ") + } + + query.delete(query.length - 2, query.length) + + if (selection != null) { + query.append(" WHERE ").append(selection) + } + + var queryString = query.toString() + + if (args != null) { + queryString = replaceQueryArgs(queryString, args) + } + + server.onSql(dbName, queryString) + } + + private fun replaceQueryArgs(query: String, args: Array?): String { + if (args == null) { + return query + } + + val builder = StringBuilder() + + var i = 0 + var argIndex = 0 + while (i < query.length) { + if (query[i] == '?' && argIndex < args.size) { + builder.append("'").append(args[argIndex]).append("'") + argIndex++ + } else { + builder.append(query[i]) + } + i++ + } + + return builder.toString() + } + data class DeviceInfo( val name: String, val packageName: String, diff --git a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt index 686df8f89c..5eb2af1d23 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt @@ -11,6 +11,9 @@ import fi.iki.elonen.NanoHTTPD import org.signal.core.util.ExceptionUtil import org.signal.core.util.logging.Log import java.lang.IllegalArgumentException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -22,7 +25,7 @@ import kotlin.math.min * to [renderTemplate]. */ internal class SpinnerServer( - context: Context, + private val context: Context, private val deviceInfo: Spinner.DeviceInfo, private val databases: Map ) : NanoHTTPD(5000) { @@ -36,6 +39,9 @@ internal class SpinnerServer( registerHelper("neq", ConditionalHelpers.neq) } + private val recentSql: MutableMap> = mutableMapOf() + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz", Locale.US) + override fun serve(session: IHTTPSession): Response { if (session.method == Method.POST) { // Needed to populate session.parameters @@ -47,11 +53,14 @@ internal class SpinnerServer( try { return when { + session.method == Method.GET && session.uri == "/css/main.css" -> newFileResponse("css/main.css", "text/css") + session.method == Method.GET && session.uri == "/js/main.js" -> newFileResponse("js/main.js", "text/javascript") session.method == Method.GET && session.uri == "/" -> getIndex(dbParam, db) session.method == Method.GET && session.uri == "/browse" -> getBrowse(dbParam, db) session.method == Method.POST && session.uri == "/browse" -> postBrowse(dbParam, db, session) session.method == Method.GET && session.uri == "/query" -> getQuery(dbParam, db) session.method == Method.POST && session.uri == "/query" -> postQuery(dbParam, db, session) + session.method == Method.GET && session.uri == "/recent" -> getRecent(dbParam, db) else -> newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "Not found") } } catch (t: Throwable) { @@ -60,6 +69,17 @@ internal class SpinnerServer( } } + fun onSql(dbName: String, sql: String) { + val commands: MutableList = recentSql[dbName] ?: mutableListOf() + + commands += QueryItem(System.currentTimeMillis(), sql) + if (commands.size > 100) { + commands.removeAt(0) + } + + recentSql[dbName] = commands + } + private fun getIndex(dbName: String, db: SupportSQLiteDatabase): Response { return renderTemplate( "overview", @@ -139,6 +159,26 @@ internal class SpinnerServer( ) } + private fun getRecent(dbName: String, db: SupportSQLiteDatabase): Response { + val queries: List? = recentSql[dbName] + ?.map { it -> + RecentQuery( + formattedTime = dateFormat.format(Date(it.time)), + query = it.query + ) + } + + return renderTemplate( + "recent", + RecentPageModel( + deviceInfo = deviceInfo, + database = dbName, + databases = databases.keys.toList(), + recentSql = queries?.reversed() + ) + ) + } + private fun postQuery(dbName: String, db: SupportSQLiteDatabase, session: IHTTPSession): Response { val action: String = session.parameters["action"]?.get(0).toString() val rawQuery: String = session.parameters["query"]?.get(0).toString() @@ -173,6 +213,14 @@ internal class SpinnerServer( return newFixedLengthResponse(output) } + private fun newFileResponse(assetPath: String, mimeType: String): Response { + return newChunkedResponse( + Response.Status.OK, + mimeType, + context.assets.open(assetPath) + ) + } + private fun Cursor.toQueryResult(queryStartTime: Long = 0): QueryResult { val numColumns = this.columnCount @@ -313,6 +361,13 @@ internal class SpinnerServer( val queryResult: QueryResult? = null ) + data class RecentPageModel( + val deviceInfo: Spinner.DeviceInfo, + val database: String, + val databases: List, + val recentSql: List? + ) + data class QueryResult( val columns: List, val rows: List>, @@ -346,4 +401,14 @@ internal class SpinnerServer( val startRow: Int, val endRow: Int ) + + data class QueryItem( + val time: Long, + val query: String + ) + + data class RecentQuery( + val formattedTime: String, + val query: String + ) }