Add a 'Recent' tab to Spinner.
This commit is contained in:
parent
acecd5f013
commit
9594be8fcf
20 changed files with 404 additions and 131 deletions
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>?)
|
||||||
|
}
|
|
@ -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) {
|
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));
|
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) {
|
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));
|
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) {
|
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));
|
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) {
|
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));
|
return traceSql("query(8)", table, selection, false, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cursor rawQuery(String sql, String[] selectionArgs) {
|
public Cursor rawQuery(String sql, String[] selectionArgs) {
|
||||||
|
DatabaseMonitor.onSql(sql, selectionArgs);
|
||||||
return traceSql("rawQuery(2a)", sql, false, () -> wrapped.rawQuery(sql, selectionArgs));
|
return traceSql("rawQuery(2a)", sql, false, () -> wrapped.rawQuery(sql, selectionArgs));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cursor rawQuery(String sql, Object[] args) {
|
public Cursor rawQuery(String sql, Object[] args) {
|
||||||
|
DatabaseMonitor.onSql(sql, args);
|
||||||
return traceSql("rawQuery(2b)", sql, false,() -> wrapped.rawQuery(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) {
|
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));
|
return traceSql("rawQueryWithFactory()", sql, false, () -> wrapped.rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cursor rawQuery(String sql, String[] selectionArgs, int initialRead, int maxRead) {
|
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));
|
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) {
|
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));
|
return traceSql("delete()", table, whereClause, true, () -> wrapped.delete(table, whereClause, whereArgs));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int update(String table, ContentValues values, String whereClause, String[] 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));
|
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 {
|
public void execSQL(String sql) throws SQLException {
|
||||||
|
DatabaseMonitor.onSql(sql, null);
|
||||||
traceSql("execSQL(1)", sql, true, () -> wrapped.execSQL(sql));
|
traceSql("execSQL(1)", sql, true, () -> wrapped.execSQL(sql));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void rawExecSQL(String sql) {
|
public void rawExecSQL(String sql) {
|
||||||
|
DatabaseMonitor.onSql(sql, null);
|
||||||
traceSql("rawExecSQL()", sql, true, () -> wrapped.rawExecSQL(sql));
|
traceSql("rawExecSQL()", sql, true, () -> wrapped.rawExecSQL(sql));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
|
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
|
||||||
|
DatabaseMonitor.onSql(sql, null);
|
||||||
traceSql("execSQL(2)", sql, true, () -> wrapped.execSQL(sql, bindArgs));
|
traceSql("execSQL(2)", sql, true, () -> wrapped.execSQL(sql, bindArgs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package org.thoughtcrime.securesms
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import leakcanary.LeakCanary
|
import leakcanary.LeakCanary
|
||||||
import org.signal.spinner.Spinner
|
import org.signal.spinner.Spinner
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseMonitor
|
||||||
import org.thoughtcrime.securesms.database.JobDatabase
|
import org.thoughtcrime.securesms.database.JobDatabase
|
||||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||||
import org.thoughtcrime.securesms.database.LogDatabase
|
import org.thoughtcrime.securesms.database.LogDatabase
|
||||||
import org.thoughtcrime.securesms.database.MegaphoneDatabase
|
import org.thoughtcrime.securesms.database.MegaphoneDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.QueryMonitor
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.util.AppSignatureUtil
|
import org.thoughtcrime.securesms.util.AppSignatureUtil
|
||||||
import shark.AndroidReferenceMatchers
|
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(
|
LeakCanary.config = LeakCanary.config.copy(
|
||||||
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
||||||
AndroidReferenceMatchers.ignoredInstanceField(
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
|
|
@ -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,
|
* 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
|
* 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
|
* 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 {
|
public final class Tracer {
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<html>
|
<html>
|
||||||
{{> head title="Browse" }}
|
{{> partials/head title="Browse" }}
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
{{> prefix isBrowse=true}}
|
{{> partials/prefix isBrowse=true}}
|
||||||
|
|
||||||
<!-- Table Selector -->
|
<!-- Table Selector -->
|
||||||
<form action="browse" method="post">
|
<form action="browse" method="post">
|
||||||
|
@ -70,6 +70,6 @@
|
||||||
<input type="submit" name="action" value="last" {{#if pagingData.lastPage}}disabled{{/if}} />
|
<input type="submit" name="action" value="last" {{#if pagingData.lastPage}}disabled{{/if}} />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{> suffix}}
|
{{> partials/suffix}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
89
spinner/lib/src/main/assets/css/main.css
Normal file
89
spinner/lib/src/main/assets/css/main.css
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
{{> head title="Error :(" }}
|
{{> partials/head title="Error :(" }}
|
||||||
<body>
|
<body>
|
||||||
Hit an exception while trying to serve the page :(
|
Hit an exception while trying to serve the page :(
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
|
@ -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>
|
|
15
spinner/lib/src/main/assets/js/main.js
Normal file
15
spinner/lib/src/main/assets/js/main.js
Normal file
|
@ -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();
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
{{> head title="Home" }}
|
{{> partials/head title="Home" }}
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
h1.collapse-header {
|
h1.collapse-header {
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
{{> prefix isOverview=true}}
|
{{> partials/prefix isOverview=true}}
|
||||||
|
|
||||||
<h1 class="collapse-header active" data-for="table-creates">Tables</h1>
|
<h1 class="collapse-header active" data-for="table-creates">Tables</h1>
|
||||||
<div id="table-creates">
|
<div id="table-creates">
|
||||||
|
@ -50,6 +50,6 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{> suffix }}
|
{{> partials/suffix }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
14
spinner/lib/src/main/assets/partials/head.hbs
Normal file
14
spinner/lib/src/main/assets/partials/head.hbs
Normal file
|
@ -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>
|
|
@ -28,4 +28,5 @@
|
||||||
<li {{#if isOverview}}class="active"{{/if}}><a href="/?db={{database}}">Overview</a></li>
|
<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 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 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>
|
</ol>
|
1
spinner/lib/src/main/assets/partials/suffix.hbs
Normal file
1
spinner/lib/src/main/assets/partials/suffix.hbs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<script src="/js/main.js" type="text/javascript"></script>
|
|
@ -1,8 +1,8 @@
|
||||||
<html>
|
<html>
|
||||||
{{> head title="Query" }}
|
{{> partials/head title="Query" }}
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
{{> prefix isQuery=true}}
|
{{> partials/prefix isQuery=true}}
|
||||||
|
|
||||||
<!-- Query Input -->
|
<!-- Query Input -->
|
||||||
<form action="query" method="post">
|
<form action="query" method="post">
|
||||||
|
@ -11,6 +11,8 @@
|
||||||
<input type="submit" name="action" value="run" />
|
<input type="submit" name="action" value="run" />
|
||||||
or
|
or
|
||||||
<input type="submit" name="action" value="analyze" />
|
<input type="submit" name="action" value="analyze" />
|
||||||
|
or
|
||||||
|
<button onclick="onFormatClicked(event)">format</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Query Result -->
|
<!-- Query Result -->
|
||||||
|
@ -38,6 +40,15 @@
|
||||||
No data.
|
No data.
|
||||||
{{/if}}
|
{{/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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
50
spinner/lib/src/main/assets/recent.hbs
Normal file
50
spinner/lib/src/main/assets/recent.hbs
Normal file
|
@ -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>
|
|
@ -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>
|
|
|
@ -34,4 +34,4 @@ fun SupportSQLiteDatabase.getTableRowCount(table: String): Int {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.signal.spinner
|
package org.signal.spinner
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteQueryBuilder
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -11,14 +13,80 @@ import java.io.IOException
|
||||||
object Spinner {
|
object Spinner {
|
||||||
val TAG: String = Log.tag(Spinner::class.java)
|
val TAG: String = Log.tag(Spinner::class.java)
|
||||||
|
|
||||||
|
private lateinit var server: SpinnerServer
|
||||||
|
|
||||||
fun init(context: Context, deviceInfo: DeviceInfo, databases: Map<String, SupportSQLiteDatabase>) {
|
fun init(context: Context, deviceInfo: DeviceInfo, databases: Map<String, SupportSQLiteDatabase>) {
|
||||||
try {
|
try {
|
||||||
SpinnerServer(context, deviceInfo, databases).start()
|
server = SpinnerServer(context, deviceInfo, databases)
|
||||||
|
server.start()
|
||||||
} catch (e: IOException) {
|
} 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(
|
data class DeviceInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
|
|
|
@ -11,6 +11,9 @@ import fi.iki.elonen.NanoHTTPD
|
||||||
import org.signal.core.util.ExceptionUtil
|
import org.signal.core.util.ExceptionUtil
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
@ -22,7 +25,7 @@ import kotlin.math.min
|
||||||
* to [renderTemplate].
|
* to [renderTemplate].
|
||||||
*/
|
*/
|
||||||
internal class SpinnerServer(
|
internal class SpinnerServer(
|
||||||
context: Context,
|
private val context: Context,
|
||||||
private val deviceInfo: Spinner.DeviceInfo,
|
private val deviceInfo: Spinner.DeviceInfo,
|
||||||
private val databases: Map<String, SupportSQLiteDatabase>
|
private val databases: Map<String, SupportSQLiteDatabase>
|
||||||
) : NanoHTTPD(5000) {
|
) : NanoHTTPD(5000) {
|
||||||
|
@ -36,6 +39,9 @@ internal class SpinnerServer(
|
||||||
registerHelper("neq", ConditionalHelpers.neq)
|
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 {
|
override fun serve(session: IHTTPSession): Response {
|
||||||
if (session.method == Method.POST) {
|
if (session.method == Method.POST) {
|
||||||
// Needed to populate session.parameters
|
// Needed to populate session.parameters
|
||||||
|
@ -47,11 +53,14 @@ internal class SpinnerServer(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return when {
|
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 == "/" -> getIndex(dbParam, db)
|
||||||
session.method == Method.GET && session.uri == "/browse" -> getBrowse(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.POST && session.uri == "/browse" -> postBrowse(dbParam, db, session)
|
||||||
session.method == Method.GET && session.uri == "/query" -> getQuery(dbParam, db)
|
session.method == Method.GET && session.uri == "/query" -> getQuery(dbParam, db)
|
||||||
session.method == Method.POST && session.uri == "/query" -> postQuery(dbParam, db, session)
|
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")
|
else -> newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "Not found")
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} 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 {
|
private fun getIndex(dbName: String, db: SupportSQLiteDatabase): Response {
|
||||||
return renderTemplate(
|
return renderTemplate(
|
||||||
"overview",
|
"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 {
|
private fun postQuery(dbName: String, db: SupportSQLiteDatabase, session: IHTTPSession): Response {
|
||||||
val action: String = session.parameters["action"]?.get(0).toString()
|
val action: String = session.parameters["action"]?.get(0).toString()
|
||||||
val rawQuery: String = session.parameters["query"]?.get(0).toString()
|
val rawQuery: String = session.parameters["query"]?.get(0).toString()
|
||||||
|
@ -173,6 +213,14 @@ internal class SpinnerServer(
|
||||||
return newFixedLengthResponse(output)
|
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 {
|
private fun Cursor.toQueryResult(queryStartTime: Long = 0): QueryResult {
|
||||||
val numColumns = this.columnCount
|
val numColumns = this.columnCount
|
||||||
|
|
||||||
|
@ -313,6 +361,13 @@ internal class SpinnerServer(
|
||||||
val queryResult: QueryResult? = null
|
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(
|
data class QueryResult(
|
||||||
val columns: List<String>,
|
val columns: List<String>,
|
||||||
val rows: List<List<String>>,
|
val rows: List<List<String>>,
|
||||||
|
@ -346,4 +401,14 @@ internal class SpinnerServer(
|
||||||
val startRow: Int,
|
val startRow: Int,
|
||||||
val endRow: Int
|
val endRow: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class QueryItem(
|
||||||
|
val time: Long,
|
||||||
|
val query: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RecentQuery(
|
||||||
|
val formattedTime: String,
|
||||||
|
val query: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue