Add a 'Recent' tab to Spinner.

fork-5.53.8
Greyson Parrelli 2022-02-21 12:39:07 -05:00
rodzic acecd5f013
commit 9594be8fcf
20 zmienionych plików z 404 dodań i 131 usunięć

Wyświetl plik

@ -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<Any>?) {
queryMonitor?.onSql(sql, args)
}
@JvmStatic
fun onQuery(distinct: Boolean, table: String, projection: Array<String>?, selection: String?, args: Array<Any>?, 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<Any>?) {
queryMonitor?.onDelete(table, selection, args)
}
@JvmStatic
fun onUpdate(table: String, values: ContentValues, selection: String?, args: Array<Any>?) {
queryMonitor?.onUpdate(table, values, selection, args)
}
}

Wyświetl plik

@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
interface QueryMonitor {
fun onSql(sql: String, args: Array<Any>?)
fun onQuery(distinct: Boolean, table: String, projection: Array<String>?, selection: String?, args: Array<Any>?, groupBy: String?, having: String?, orderBy: String?, limit: String?)
fun onDelete(table: String, selection: String?, args: Array<Any>?)
fun onUpdate(table: String, values: ContentValues, selection: String?, args: Array<Any>?)
}

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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<Any>?) {
Spinner.onSql("signal", sql, args)
}
override fun onQuery(distinct: Boolean, table: String, projection: Array<String>?, selection: String?, args: Array<Any>?, 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<Any>?) {
Spinner.onDelete("signal", table, selection, args)
}
override fun onUpdate(table: String, values: ContentValues, selection: String?, args: Array<Any>?) {
Spinner.onUpdate("signal", table, values, selection, args)
}
})
LeakCanary.config = LeakCanary.config.copy(
referenceMatchers = AndroidReferenceMatchers.appDefaults +
AndroidReferenceMatchers.ignoredInstanceField(

Wyświetl plik

@ -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 {

Wyświetl plik

@ -1,8 +1,8 @@
<html>
{{> head title="Browse" }}
{{> partials/head title="Browse" }}
<body>
{{> prefix isBrowse=true}}
{{> partials/prefix isBrowse=true}}
<!-- Table Selector -->
<form action="browse" method="post">
@ -70,6 +70,6 @@
<input type="submit" name="action" value="last" {{#if pagingData.lastPage}}disabled{{/if}} />
</form>
{{> suffix}}
{{> partials/suffix}}
</body>
</html>

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -1,5 +1,5 @@
<html>
{{> head title="Error :(" }}
{{> partials/head title="Error :(" }}
<body>
Hit an exception while trying to serve the page :(
<hr/>

Wyświetl plik

@ -1,99 +0,0 @@
<head>
<title>Spinner - {{ title }}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌀</text></svg>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap" rel="stylesheet">
<style type="text/css">
html, body {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
select, input {
font-family: 'JetBrains Mono', monospace;
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;
}
</style>
</head>

Wyświetl plik

@ -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();

Wyświetl plik

@ -1,5 +1,5 @@
<html>
{{> head title="Home" }}
{{> partials/head title="Home" }}
<style type="text/css">
h1.collapse-header {
@ -12,7 +12,7 @@
<body>
{{> prefix isOverview=true}}
{{> partials/prefix isOverview=true}}
<h1 class="collapse-header active" data-for="table-creates">Tables</h1>
<div id="table-creates">
@ -50,6 +50,6 @@
{{/if}}
</div>
{{> suffix }}
{{> partials/suffix }}
</body>
</html>

Wyświetl plik

@ -0,0 +1,14 @@
<head>
<title>Spinner - {{ title }}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌀</text></svg>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<link href="/css/main.css" rel="stylesheet">
<style type="text/css">
</style>
</head>

Wyświetl plik

@ -28,4 +28,5 @@
<li {{#if isOverview}}class="active"{{/if}}><a href="/?db={{database}}">Overview</a></li>
<li {{#if isBrowse}}class="active"{{/if}}><a href="/browse?db={{database}}">Browse</a></li>
<li {{#if isQuery}}class="active"{{/if}}><a href="/query?db={{database}}">Query</a></li>
<li {{#if isRecent}}class="active"{{/if}}><a href="/recent?db={{database}}">Recent</a></li>
</ol>

Wyświetl plik

@ -0,0 +1 @@
<script src="/js/main.js" type="text/javascript"></script>

Wyświetl plik

@ -1,8 +1,8 @@
<html>
{{> head title="Query" }}
{{> partials/head title="Query" }}
<body>
{{> prefix isQuery=true}}
{{> partials/prefix isQuery=true}}
<!-- Query Input -->
<form action="query" method="post">
@ -11,6 +11,8 @@
<input type="submit" name="action" value="run" />
or
<input type="submit" name="action" value="analyze" />
or
<button onclick="onFormatClicked(event)">format</button>
</form>
<!-- Query Result -->
@ -38,6 +40,15 @@
No data.
{{/if}}
{{> suffix}}
{{> partials/suffix}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql-formatter/4.0.2/sql-formatter.min.js" integrity="sha512-JPoVzOHQvXbB4+lOX6GOBM3xOZhwAMKRYn2G0VpfPcwIixAAvPL+HKuaFuevm+i6Q4GktSKY/CxlcB/1BaV/6Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
function onFormatClicked(e) {
e.preventDefault();
const queryInput = document.querySelector('.query-input')
queryInput.value = sqlFormatter.format(queryInput.value).replaceAll("! =", "!=").replaceAll("| |", "||");
}
</script>
</body>
</html>

Wyświetl plik

@ -0,0 +1,50 @@
<html>
{{> partials/head title="Home" }}
<style type="text/css">
h1.collapse-header {
font-size: 1.35rem;
}
h2.collapse-header {
font-size: 1.15rem;
}
table.recent {
width: 100%;
}
</style>
<body>
{{> partials/prefix isRecent=true}}
{{#if recentSql}}
<table class="recent">
{{#each recentSql}}
<tr>
<td>
{{formattedTime}}
</td>
<td>
<form action="query" method="post">
<input type="hidden" name="db" value="{{database}}" />
<input type="hidden" name="query" value="{{query}}" />
<input type="submit" name="action" value="run" />
<input type="submit" name="action" value="analyze" />
</form>
</td>
<td>{{query}}</td>
</tr>
{{/each}}
</table>
{{else}}
No recent queries.
{{/if}}
{{> partials/suffix }}
<script>
function onAnalyzeClicked(id) {
document.getElementById
}
</script>
</body>
</html>

Wyświetl plik

@ -1,17 +0,0 @@
<script type="text/javascript">
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();
</script>

Wyświetl plik

@ -34,4 +34,4 @@ fun SupportSQLiteDatabase.getTableRowCount(table: String): Int {
0
}
}
}
}

Wyświetl plik

@ -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<String, SupportSQLiteDatabase>) {
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<Any>?) {
server.onSql(dbName, replaceQueryArgs(sql, args))
}
fun onQuery(dbName: String, distinct: Boolean, table: String, projection: Array<String>?, selection: String?, args: Array<Any>?, 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<Any>?) {
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<Any>?) {
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<Any>?): 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,

Wyświetl plik

@ -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<String, SupportSQLiteDatabase>
) : NanoHTTPD(5000) {
@ -36,6 +39,9 @@ internal class SpinnerServer(
registerHelper("neq", ConditionalHelpers.neq)
}
private val recentSql: MutableMap<String, MutableList<QueryItem>> = 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<QueryItem> = 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<RecentQuery>? = 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<String>,
val recentSql: List<RecentQuery>?
)
data class QueryResult(
val columns: List<String>,
val rows: List<List<String>>,
@ -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
)
}