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) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
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>
|
||||
{{> head title="Error :(" }}
|
||||
{{> partials/head title="Error :(" }}
|
||||
<body>
|
||||
Hit an exception while trying to serve the page :(
|
||||
<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>
|
||||
{{> 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>
|
||||
|
|
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 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>
|
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>
|
||||
{{> 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>
|
||||
|
|
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue