Replace Flipper with Spinner.

fork-5.53.8
Greyson Parrelli 2022-02-14 14:18:03 -05:00
rodzic 4bdea886e3
commit 874067909d
46 zmienionych plików z 1558 dodań i 297 usunięć

Wyświetl plik

@ -51,6 +51,13 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
</JetCodeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="BRACE_STYLE" value="5" />
<option name="CLASS_BRACE_STYLE" value="5" />

Wyświetl plik

@ -75,18 +75,18 @@ def abiPostFix = ['universal' : 0,
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
def selectableVariants = [
'nightlyProdFlipper',
'nightlyProdSpinner',
'nightlyProdPerf',
'nightlyProdRelease',
'playProdDebug',
'playProdFlipper',
'playProdSpinner',
'playProdPerf',
'playProdRelease',
'playStagingDebug',
'playStagingFlipper',
'playStagingSpinner',
'playStagingPerf',
'playStagingRelease',
'websiteProdFlipper',
'websiteProdSpinner',
'websiteProdRelease',
]
@ -250,12 +250,12 @@ android {
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
}
flipper {
spinner {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
}
release {
minifyEnabled true
@ -509,9 +509,8 @@ dependencies {
}
implementation libs.dnsjava
flipperImplementation libs.facebook.flipper
flipperImplementation libs.facebook.soloader
flipperImplementation libs.square.leakcanary
spinnerImplementation project(":spinner")
spinnerImplementation libs.square.leakcanary
testImplementation testLibs.junit.junit
testImplementation testLibs.assertj.core

Wyświetl plik

@ -1,271 +0,0 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
import com.facebook.flipper.plugins.databases.DatabaseDriver;
import net.zetetic.database.DatabaseUtils;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.util.Hex;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
* and made to work with SqlCipher. Unfortunately I couldn't use it directly, nor subclass it.
*/
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
public FlipperSqlCipherAdapter(Context context) {
super(context);
}
@Override
public List<Descriptor> getDatabases() {
try {
SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance());
SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
SignalDatabaseOpenHelper metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
return Arrays.asList(new Descriptor(mainOpenHelper),
new Descriptor(keyValueOpenHelper),
new Descriptor(megaphoneOpenHelper),
new Descriptor(jobManagerOpenHelper),
new Descriptor(metricsOpenHelper));
} catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e);
}
return Collections.emptyList();
}
@Override
public List<String> getTableNames(Descriptor descriptor) {
SQLiteDatabase db = descriptor.getReadable();
List<String> tableNames = new ArrayList<>();
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" })) {
while (cursor != null && cursor.moveToNext()) {
tableNames.add(cursor.getString(0));
}
}
return tableNames;
}
@Override
public DatabaseGetTableDataResponse getTableData(Descriptor descriptor, String table, String order, boolean reverse, int start, int count) {
SQLiteDatabase db = descriptor.getReadable();
long total = DatabaseUtils.queryNumEntries(db, table);
String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null;
String limitBy = start + ", " + count;
try (Cursor cursor = db.query(table, null, null, null, null, null, orderBy, limitBy)) {
String[] columnNames = cursor.getColumnNames();
List<List<Object>> rows = cursorToList(cursor);
return new DatabaseGetTableDataResponse(Arrays.asList(columnNames), rows, start, rows.size(), total);
}
}
@Override
public DatabaseGetTableStructureResponse getTableStructure(Descriptor descriptor, String table) {
SQLiteDatabase db = descriptor.getReadable();
Map<String, String> foreignKeyValues = new HashMap<>();
try(Cursor cursor = db.rawQuery("PRAGMA foreign_key_list(" + table + ")", null)) {
while (cursor != null && cursor.moveToNext()) {
String from = cursor.getString(cursor.getColumnIndex("from"));
String to = cursor.getString(cursor.getColumnIndex("to"));
String tableName = cursor.getString(cursor.getColumnIndex("table")) + "(" + to + ")";
foreignKeyValues.put(from, tableName);
}
}
List<String> structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
List<List<Object>> structureValues = new ArrayList<>();
try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
while (cursor != null && cursor.moveToNext()) {
String columnName = cursor.getString(cursor.getColumnIndex("name"));
String foreignKey = foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null;
structureValues.add(Arrays.asList(columnName,
cursor.getString(cursor.getColumnIndex("type")),
cursor.getInt(cursor.getColumnIndex("notnull")) == 0,
getObjectFromColumnIndex(cursor, cursor.getColumnIndex("dflt_value")),
cursor.getInt(cursor.getColumnIndex("pk")) == 1,
foreignKey));
}
}
List<String> indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
List<List<Object>> indexesValues = new ArrayList<>();
try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) {
List<String> indexedColumnNames = new ArrayList<>();
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
try(Cursor indexInfoCursor = db.rawQuery("PRAGMA index_info(" + indexName + ")", null)) {
while (indexInfoCursor.moveToNext()) {
indexedColumnNames.add(indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name")));
}
}
indexesValues.add(Arrays.asList(indexName,
indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1,
TextUtils.join(",", indexedColumnNames)));
}
return new DatabaseGetTableStructureResponse(structureColumns, structureValues, indexesColumns, indexesValues);
}
@Override
public DatabaseGetTableInfoResponse getTableInfo(Descriptor databaseDescriptor, String table) {
SQLiteDatabase db = databaseDescriptor.getReadable();
try (Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[] { table })) {
cursor.moveToFirst();
return new DatabaseGetTableInfoResponse(cursor.getString(cursor.getColumnIndex("sql")));
}
}
@Override
public DatabaseExecuteSqlResponse executeSQL(Descriptor descriptor, String query) {
SQLiteDatabase db = descriptor.getWritable();
String firstWordUpperCase = getFirstWord(query).toUpperCase();
switch (firstWordUpperCase) {
case "UPDATE":
case "DELETE":
return executeUpdateDelete(db, query);
case "INSERT":
return executeInsert(db, query);
case "SELECT":
case "PRAGMA":
case "EXPLAIN":
return executeSelect(db, query);
default:
return executeRawQuery(db, query);
}
}
private static String getFirstWord(String s) {
s = s.trim();
int firstSpace = s.indexOf(' ');
return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
}
private static DatabaseExecuteSqlResponse executeUpdateDelete(SQLiteDatabase database, String query) {
SQLiteStatement statement = database.compileStatement(query);
int count = statement.executeUpdateDelete();
return DatabaseExecuteSqlResponse.successfulUpdateDelete(count);
}
private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) {
SQLiteStatement statement = database.compileStatement(query);
long insertedId = statement.executeInsert();
return DatabaseExecuteSqlResponse.successfulInsert(insertedId);
}
private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) {
try (Cursor cursor = database.rawQuery(query, null)) {
String[] columnNames = cursor.getColumnNames();
List<List<Object>> rows = cursorToList(cursor);
return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows);
}
}
private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) {
database.execSQL(query);
return DatabaseExecuteSqlResponse.successfulRawQuery();
}
private static @NonNull List<List<Object>> cursorToList(Cursor cursor) {
List<List<Object>> rows = new ArrayList<>();
int numColumns = cursor.getColumnCount();
while (cursor.moveToNext()) {
List<Object> values = new ArrayList<>(numColumns);
for (int column = 0; column < numColumns; column++) {
values.add(getObjectFromColumnIndex(cursor, column));
}
rows.add(values);
}
return rows;
}
private static @Nullable Object getObjectFromColumnIndex(Cursor cursor, int column) {
switch (cursor.getType(column)) {
case Cursor.FIELD_TYPE_NULL:
return null;
case Cursor.FIELD_TYPE_INTEGER:
return cursor.getLong(column);
case Cursor.FIELD_TYPE_FLOAT:
return cursor.getDouble(column);
case Cursor.FIELD_TYPE_BLOB:
byte[] blob = cursor.getBlob(column);
String bytes = blob != null ? "(blob) " + Hex.toStringCondensed(Arrays.copyOf(blob, Math.min(blob.length, 32))) : null;
if (bytes != null && bytes.length() == 32 && blob.length > 32) {
bytes += "...";
}
return bytes;
case Cursor.FIELD_TYPE_STRING:
default:
return cursor.getString(column);
}
}
static class Descriptor implements DatabaseDescriptor {
private final SignalDatabaseOpenHelper sqlCipherOpenHelper;
Descriptor(@NonNull SignalDatabaseOpenHelper sqlCipherOpenHelper) {
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
}
@Override
public String name() {
return sqlCipherOpenHelper.getDatabaseName();
}
public @NonNull SQLiteDatabase getReadable() {
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
public @NonNull SQLiteDatabase getWritable() {
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
}
}

Wyświetl plik

@ -4,7 +4,7 @@
package="org.thoughtcrime.securesms">
<application
android:name=".FlipperApplicationContext"
android:name=".SpinnerApplicationContext"
tools:replace="android:name">
<activity

Wyświetl plik

@ -1,25 +1,37 @@
package org.thoughtcrime.securesms
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader
import android.os.Build
import leakcanary.LeakCanary
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
import org.signal.spinner.Spinner
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.SignalDatabase
import org.thoughtcrime.securesms.util.AppSignatureUtil
import shark.AndroidReferenceMatchers
class FlipperApplicationContext : ApplicationContext() {
class SpinnerApplicationContext : ApplicationContext() {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
val client = AndroidFlipperClient.getInstance(this)
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
client.addPlugin(SharedPreferencesFlipperPlugin(this))
client.start()
Spinner.init(
this,
Spinner.DeviceInfo(
name = "${Build.MODEL} (Android ${Build.VERSION.RELEASE}, API ${Build.VERSION.SDK_INT})",
packageName = "$packageName (${AppSignatureUtil.getAppSignature(this).or("Unknown")})",
appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.CANONICAL_VERSION_CODE}, ${BuildConfig.GIT_HASH})"
),
linkedMapOf(
"signal" to SignalDatabase.rawDatabase,
"jobmanager" to JobDatabase.getInstance(this).sqlCipherDatabase,
"keyvalue" to KeyValueDatabase.getInstance(this).sqlCipherDatabase,
"megaphones" to MegaphoneDatabase.getInstance(this).sqlCipherDatabase,
"localmetrics" to LocalMetricsDatabase.getInstance(this).sqlCipherDatabase,
"logs" to LogDatabase.getInstance(this).sqlCipherDatabase,
)
)
LeakCanary.config = LeakCanary.config.copy(
referenceMatchers = AndroidReferenceMatchers.appDefaults +

Wyświetl plik

@ -115,8 +115,6 @@ dependencyResolutionManagement {
alias('stickyheadergrid').to('com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4')
alias('circular-progress-button').to('com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2')
alias('dnsjava').to('dnsjava:dnsjava:2.1.9')
alias('facebook-flipper').to('com.facebook.flipper:flipper:0.91.0')
alias('facebook-soloader').to('com.facebook.soloader:soloader:0.10.1')
// Mp4Parser
alias('mp4parser-isoparser').to('org.mp4parser', 'isoparser').versionRef('mp4parser')

Wyświetl plik

@ -23,6 +23,14 @@
<sha256 value="b7730754793e2fa510ddb10b7514e65f8706e4ec4b100acf7e4215f0bd5519b4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.activity" name="activity" version="1.4.0">
<artifact name="activity-1.4.0.aar">
<sha256 value="89dc38e0cdbd11f328c7d0b3b021ddb387ca9da0d49f14b18c91e300c45ed79c" origin="Generated by Gradle"/>
</artifact>
<artifact name="activity-1.4.0.module">
<sha256 value="b38ce719cf1862701ab54b48405fc832a8ca8d4aacb2ce0d37456d0aff329147" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-ktx" version="1.2.2">
<artifact name="activity-ktx-1.2.2.aar">
<sha256 value="9829e13d6a6b045b03b21a330512e091dc76eb5b3ded0d88d1ab0509cf84a50e" origin="Generated by Gradle"/>
@ -31,6 +39,14 @@
<sha256 value="92f4431091650b5a67cc4f654bd9b822c585cf4262180912f075779f07a04ba6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-ktx" version="1.4.0">
<artifact name="activity-ktx-1.4.0.aar">
<sha256 value="3f301941f37a90b4bc553dbbe84e7464a97c0d21df6cf2d6c0cb1b2c07349f33" origin="Generated by Gradle"/>
</artifact>
<artifact name="activity-ktx-1.4.0.module">
<sha256 value="44950669cc9951b30ca8f9dd426fff3d660672262e74afac785bded4aacc5a03" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.0.0">
<artifact name="annotation-1.0.0.jar">
<sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle"/>
@ -54,6 +70,14 @@
<sha256 value="b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0">
<artifact name="annotation-experimental-1.1.0.aar">
<sha256 value="0157de61a2064047896a058080f3fd67ba57ad9a94857b3f7a363660243e3f90" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotation-experimental-1.1.0.module">
<sha256 value="0361d1526a4d7501255e19779e09e93cdbd07fee0e2f5c50b7a137432d510119" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.2.0">
<artifact name="appcompat-1.2.0.aar">
<sha256 value="3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70" origin="Generated by Gradle"/>
@ -213,6 +237,14 @@
<sha256 value="e3877fa529fe29177f34a26e0790ed35544848b0c7503bfed30b2539f1686d65" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.7.0">
<artifact name="core-1.7.0.aar">
<sha256 value="aaf6734226fff923784f92f65d78a2984dbf17534138855c5ce2038f18656e0b" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-1.7.0.module">
<sha256 value="988f820899d5a4982e5c878ca1cd417970ace332ea2ff72f5be19b233fa0e788" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.5.0">
<artifact name="core-ktx-1.5.0.aar">
<sha256 value="5964cfe7a4882da2a00fb6ca3d3a072d04139208186f7bc4b3cb66022764fc42" origin="Generated by Gradle"/>
@ -1507,6 +1539,21 @@
<sha256 value="23f5c982e1c7771423d37d52c774e8d2e80fd7ea7305ebe448797a96f67e6fca" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.jknack" name="handlebars" version="4.0.6">
<artifact name="handlebars-4.0.6.jar">
<sha256 value="f20c47fd6572170951e83af1c11a5c12e724fa60535d62219bf2f762620d5781" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.jknack" name="handlebars" version="4.0.7">
<artifact name="handlebars-4.0.7.jar">
<sha256 value="d9b155fe8c8ddb0f9b3e5b156b5909dcd4c89d93defb2f72d0961aa633ad838f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.jknack" name="handlebars" version="4.3.0">
<artifact name="handlebars-4.3.0.jar">
<sha256 value="9441bb30635ae1db3a73d793accfe91ed4c2a4edec39750557f11f1debbc8eb7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.shyiko.klob" name="klob" version="0.2.1">
<artifact name="klob-0.2.1.jar">
<sha256 value="2f6174e3049008f263fd832813390df645ac5c7cfa79f170ace58690810476f2" origin="Generated by Gradle"/>
@ -1810,6 +1857,11 @@
<sha256 value="e6dd072f9d3fe02a4600688380bd422bdac184caf6fe2418cfdd0934f09432aa" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="listenablefuture" version="1.0">
<artifact name="listenablefuture-1.0.jar">
<sha256 value="e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="listenablefuture" version="9999.0-empty-to-avoid-conflict-with-guava">
<artifact name="listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar">
<sha256 value="b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99" origin="Generated by Gradle"/>
@ -2562,11 +2614,21 @@
<sha256 value="a32de739cfdf515774e696f91aa9697d2e7731e5cb5045ca8a4b657f8b1b4fb4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.antlr" name="antlr4-runtime" version="4.5.1-1">
<artifact name="antlr4-runtime-4.5.1-1.jar">
<sha256 value="ffca72bc2a25bb2b0c80a58cee60530a78be17da739bb6c91a8c2e3584ca099e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.antlr" name="antlr4-runtime" version="4.5.2-1">
<artifact name="antlr4-runtime-4.5.2-1.jar">
<sha256 value="e831413004bceed7d915c3a175927b1daabc4974b7b8a6f87bbce886d3550398" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.antlr" name="antlr4-runtime" version="4.7.1">
<artifact name="antlr4-runtime-4.7.1.jar">
<sha256 value="43516d19beae35909e04d06af6c0c58c17bc94e0070c85e8dc9929ca640dc91d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.ant" name="ant" version="1.10.9">
<artifact name="ant-1.10.9.jar">
<sha256 value="0715478af585ea80a18985613ebecdc7922122d45b2c3c970ff9b352cddb75fc" origin="Generated by Gradle"/>
@ -2597,6 +2659,11 @@
<sha256 value="0aeb625c948c697ea7b205156e112363b59ed5e2551212cd4e460bdb72c7c06e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.commons" name="commons-lang3" version="3.1">
<artifact name="commons-lang3-3.1.jar">
<sha256 value="131f0519a8e4602e47cf024bfd7e0834bcf5592a7207f9a2fdb711d4f5afc166" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.httpcomponents" name="httpclient" version="4.5.6">
<artifact name="httpclient-4.5.6.jar">
<sha256 value="c03f813195e7a80e3608d0ddd8da80b21696a4c92a6a2298865bf149071551c7" origin="Generated by Gradle"/>
@ -3192,6 +3259,14 @@
<sha256 value="4a80f7a521f70a87798e74416b596336c76d8306594172a4cf142c16e1720081" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.4.1">
<artifact name="kotlinx-coroutines-android-1.4.1.jar">
<sha256 value="d4cadb673b2101f1ee5fbc147956ac78b1cfd9cc255fb53d3aeb88dff11d99ca" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-android-1.4.1.module">
<sha256 value="d576909e18f54010f0429aea9e5155a27462a53925bdc4f6fccd96bfccff5145" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.5.0">
<artifact name="kotlinx-coroutines-android-1.5.0.jar">
<sha256 value="7099198391d673c199fea084423d9f3fdc79470acba19111330c7f88504279c7" origin="Generated by Gradle"/>
@ -3205,6 +3280,11 @@
<sha256 value="f8c8b7485d4a575e38e5e94945539d1d4eccd3228a199e1a9aa094e8c26174ee" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.4.1">
<artifact name="kotlinx-coroutines-core-1.4.1.module">
<sha256 value="3c00e44941f134b18cadbc5f18ab7b7f23d3ef1f78af95e344cb9c605db21a44" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.5.0">
<artifact name="kotlinx-coroutines-core-1.5.0.jar">
<sha256 value="6f738012913d3d4bc18408a5011108d4744a72b6233662ee4d4dd50da9550b8d" origin="Generated by Gradle"/>
@ -3213,6 +3293,14 @@
<sha256 value="d8a26a896da32fb1f8c3f13fe41cb798a612a1c1ddf3a555d82ee1ff16ef13d3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.4.1">
<artifact name="kotlinx-coroutines-core-jvm-1.4.1.jar">
<sha256 value="6d2f87764b6638f27aff12ed380db4b63c9d46ba55dc32683a650598fa5a3e22" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-core-jvm-1.4.1.module">
<sha256 value="e6a6f8ffb1a40bc76a935cdd5ecfcf5485b5cd0ec2883044a4760cbfe3315707" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.5.0">
<artifact name="kotlinx-coroutines-core-jvm-1.5.0.jar">
<sha256 value="78d6cc7135f84d692ff3752fcfd1fa1bbe0940d7df70652e4f1eaeec0c78afbb" origin="Generated by Gradle"/>
@ -3221,6 +3309,14 @@
<sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-metadata" version="1.4.1">
<artifact name="kotlinx-coroutines-core-metadata-1.4.1.jar">
<sha256 value="22a9e8c5d2e3dbf8e539f1bd0e0516845e6cac0e5708476cc33b1bb994f2b650" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-core-metadata-1.4.1.module">
<sha256 value="c075dc88140a6ab48a6657113fa0b587ddfa82640a4672247dafc0de208f1192" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-serialization-core" version="1.1.0">
<artifact name="kotlinx-serialization-core-1.1.0.module">
<sha256 value="a21890616c068b55580ca3cf008b3d5d7f9613c980b754b4ad5a5bf74e8babf5" origin="Generated by Gradle"/>
@ -3340,6 +3436,16 @@
<sha256 value="4be648c50456fba4686ba825000d628c1d805a3b92272ba9ad5b697dfa43036b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mozilla" name="rhino" version="1.7.7">
<artifact name="rhino-1.7.7.jar">
<sha256 value="73b8d6bbbd1a6a3a87ea0eea301996deac83f8d40b404518a10afd4d320b5b31" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mozilla" name="rhino" version="1.7R4">
<artifact name="rhino-1.7R4.jar">
<sha256 value="eb4cbd05a48ee4448825da229e94115e68adc6c5638d29022914e1178c60a6c4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mp4parser" name="isoparser" version="1.9.39">
<artifact name="isoparser-1.9.39.jar">
<sha256 value="a3a7172648f1ac4b2a369ecca2861317e472179c842a5217b08643ba0a1dfa12" origin="Generated by Gradle"/>
@ -3355,6 +3461,16 @@
<sha256 value="da5151cfc3bf491d550fb9127bba22736f4b7416058d58a1a5fcfdfa3673876d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.nanohttpd" name="nanohttpd" version="2.3.1">
<artifact name="nanohttpd-2.3.1.jar">
<sha256 value="de864c47818157141a24c9acb36df0c47d7bf15b7ff48c90610f3eb4e5df0e58" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.nanohttpd" name="nanohttpd-webserver" version="2.3.1">
<artifact name="nanohttpd-webserver-2.3.1.jar">
<sha256 value="c2a094648a63d55a9577934c85a79e5ea28b8b138b4915d3494c75aa23ca0ab9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.objenesis" name="objenesis" version="2.6">
<artifact name="objenesis-2.6.jar">
<sha256 value="5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d" origin="Generated by Gradle"/>
@ -3550,11 +3666,21 @@
<sha256 value="fc0e57dec3836f2560b8d26dae8d2e330052b920b3028b49dfbd9c6b9a8ed0e2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="1.6.4">
<artifact name="slf4j-api-1.6.4.jar">
<sha256 value="367b909030f714ee1176ab096b681e06348f03385e98d1bce0ed801b5452357e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="1.7.24">
<artifact name="slf4j-api-1.7.24.jar">
<sha256 value="baf3c7fe15fefeaf9e5b000d94547379dc48370f22a8797e239c127e7d7756ec" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="1.7.32">
<artifact name="slf4j-api-1.7.32.jar">
<sha256 value="3624f8474c1af46d75f98bc097d7864a323c81b3808aa43689a6e1c601c027be" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.smali" name="dexlib2" version="2.2.4">
<artifact name="dexlib2-2.2.4.jar">
<sha256 value="cb2677bfb66cfbc954e96e806ac6bda13051ad37754f9d1bcce38514e50e41e6" origin="Generated by Gradle"/>

Wyświetl plik

@ -14,6 +14,8 @@ include ':image-editor'
include ':image-editor-app'
include ':donations'
include ':donations-app'
include ':spinner'
include ':spinner-app'
project(':app').name = 'Signal-Android'
project(':paging').projectDir = file('paging/lib')
@ -30,6 +32,9 @@ project(':image-editor-app').projectDir = file('image-editor/app')
project(':donations').projectDir = file('donations/lib')
project(':donations-app').projectDir = file('donations/app')
project(':spinner').projectDir = file('spinner/lib')
project(':spinner-app').projectDir = file('spinner/app')
rootProject.name='Signal'
apply from: 'dependencies.gradle'

21
spinner/README.md 100644
Wyświetl plik

@ -0,0 +1,21 @@
# Spinner
Spinner is a development tool that lets you inspect and run queries against an app's database(s) in a convenient web interface.
## Getting Started
Install one of the spinner build variants (e.g. `./gradlew installPlayProdSpinner`) and run the following adb command:
```bash
adb forward tcp:5000 tcp:5000
```
Then, navigate to `localhost:5000` in your web browser.
Magic!
## How does it work?
Spinner is just a [NanoHttpd](https://github.com/NanoHttpd/nanohttpd) server that runs a little webapp in the background.
You initialize Spinner in `Application.onCreate` with a list of databases you wish to let it run queries against.
Then, you can use the `adb forward` command to route the Android device's port to a port on your local machine.
## What's with the name?
It's a riff on Flipper, a development tool we used to use. It was very useful, but also wildly unstable (at least on Linux).

Wyświetl plik

@ -0,0 +1,61 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'org.jlleitschuh.gradle.ktlint'
}
repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content {
includeGroupByRegex "org\\.signal.*"
}
}
}
ktlint {
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
version = "0.43.2"
}
android {
buildToolsVersion BUILD_TOOL_VERSION
compileSdkVersion COMPILE_SDK
defaultConfig {
applicationId "org.signal.spinnertest"
versionCode 1
versionName "1.0"
minSdkVersion MINIMUM_SDK
targetSdkVersion TARGET_SDK
multiDexEnabled true
}
kotlinOptions {
jvmTarget = '1.8'
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
}
}
dependencies {
coreLibraryDesugaring libs.android.tools.desugar
implementation "androidx.activity:activity-ktx:1.2.2"
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation libs.material.material
implementation libs.androidx.constraintlayout
implementation libs.signal.android.database.sqlcipher
implementation libs.androidx.sqlite
testImplementation testLibs.junit.junit
implementation project(':spinner')
}

Wyświetl plik

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="org.signal.spinnertest">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PagingTest">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Wyświetl plik

@ -0,0 +1,36 @@
package org.signal.spinnertest
import android.database.sqlite.SQLiteDatabase
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.contentValuesOf
import org.signal.spinner.Spinner
import java.util.UUID
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = SpinnerTestSqliteOpenHelper(applicationContext)
// insertMockData(db.writableDatabase)
Spinner.init(
this,
Spinner.DeviceInfo(
name = "${Build.MODEL} (API ${Build.VERSION.SDK_INT})",
packageName = packageName,
appVersion = "0.1"
),
mapOf("main" to db)
)
}
private fun insertMockData(db: SQLiteDatabase) {
for (i in 1..10000) {
db.insert("test", null, contentValuesOf("col1" to UUID.randomUUID().toString(), "col2" to UUID.randomUUID().toString()))
}
}
}

Wyświetl plik

@ -0,0 +1,180 @@
package org.signal.spinnertest
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.database.sqlite.SQLiteTransactionListener
import android.os.CancellationSignal
import android.util.Pair
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteStatement
import java.util.Locale
class SpinnerTestSqliteOpenHelper(context: Context?) :
SQLiteOpenHelper(context, "test", null, 2), SupportSQLiteDatabase {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE test (id INTEGER PRIMARY KEY, col1 TEXT, col2 TEXT)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) {
db.execSQL("CREATE INDEX test_col1_index ON test (col1)")
}
}
override fun compileStatement(sql: String?): SupportSQLiteStatement {
TODO("Not yet implemented")
}
override fun beginTransaction() {
writableDatabase.beginTransaction()
}
override fun beginTransactionNonExclusive() {
writableDatabase.beginTransactionNonExclusive()
}
override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener?) {
writableDatabase.beginTransactionWithListener(transactionListener)
}
override fun beginTransactionWithListenerNonExclusive(transactionListener: SQLiteTransactionListener?) {
writableDatabase.beginTransactionWithListenerNonExclusive(transactionListener)
}
override fun endTransaction() {
writableDatabase.endTransaction()
}
override fun setTransactionSuccessful() {
writableDatabase.setTransactionSuccessful()
}
override fun inTransaction(): Boolean {
return writableDatabase.inTransaction()
}
override fun isDbLockedByCurrentThread(): Boolean {
return writableDatabase.isDbLockedByCurrentThread
}
override fun yieldIfContendedSafely(): Boolean {
return writableDatabase.yieldIfContendedSafely()
}
override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long): Boolean {
return writableDatabase.yieldIfContendedSafely(sleepAfterYieldDelay)
}
override fun getVersion(): Int {
return writableDatabase.version
}
override fun setVersion(version: Int) {
writableDatabase.version = version
}
override fun getMaximumSize(): Long {
return writableDatabase.maximumSize
}
override fun setMaximumSize(numBytes: Long): Long {
writableDatabase.maximumSize = numBytes
return writableDatabase.maximumSize
}
override fun getPageSize(): Long {
return writableDatabase.pageSize
}
override fun setPageSize(numBytes: Long) {
writableDatabase.pageSize = numBytes
}
override fun query(query: String?): Cursor {
return readableDatabase.rawQuery(query, null)
}
override fun query(query: String?, bindArgs: Array<out Any>?): Cursor {
return readableDatabase.rawQuery(query, bindArgs?.map { it.toString() }?.toTypedArray())
}
override fun query(query: SupportSQLiteQuery?): Cursor {
TODO("Not yet implemented")
}
override fun query(query: SupportSQLiteQuery?, cancellationSignal: CancellationSignal?): Cursor {
TODO("Not yet implemented")
}
override fun insert(table: String?, conflictAlgorithm: Int, values: ContentValues?): Long {
return writableDatabase.insertWithOnConflict(table, null, values, conflictAlgorithm)
}
override fun delete(table: String?, whereClause: String?, whereArgs: Array<out Any>?): Int {
return writableDatabase.delete(table, whereClause, whereArgs?.map { it.toString() }?.toTypedArray())
}
override fun update(table: String?, conflictAlgorithm: Int, values: ContentValues?, whereClause: String?, whereArgs: Array<out Any>?): Int {
return writableDatabase.updateWithOnConflict(table, values, whereClause, whereArgs?.map { it.toString() }?.toTypedArray(), conflictAlgorithm)
}
override fun execSQL(sql: String?) {
writableDatabase.execSQL(sql)
}
override fun execSQL(sql: String?, bindArgs: Array<out Any>?) {
writableDatabase.execSQL(sql, bindArgs?.map { it.toString() }?.toTypedArray())
}
override fun isReadOnly(): Boolean {
return readableDatabase.isReadOnly
}
override fun isOpen(): Boolean {
return readableDatabase.isOpen
}
override fun needUpgrade(newVersion: Int): Boolean {
return readableDatabase.needUpgrade(newVersion)
}
override fun getPath(): String {
return readableDatabase.path
}
override fun setLocale(locale: Locale?) {
writableDatabase.setLocale(locale)
}
override fun setMaxSqlCacheSize(cacheSize: Int) {
writableDatabase.setMaxSqlCacheSize(cacheSize)
}
override fun setForeignKeyConstraintsEnabled(enable: Boolean) {
writableDatabase.setForeignKeyConstraintsEnabled(enable)
}
override fun enableWriteAheadLogging(): Boolean {
return writableDatabase.enableWriteAheadLogging()
}
override fun disableWriteAheadLogging() {
writableDatabase.disableWriteAheadLogging()
}
override fun isWriteAheadLoggingEnabled(): Boolean {
return readableDatabase.isWriteAheadLoggingEnabled
}
override fun getAttachedDbs(): MutableList<Pair<String, String>> {
return readableDatabase.attachedDbs
}
override fun isDatabaseIntegrityOk(): Boolean {
return readableDatabase.isDatabaseIntegrityOk
}
}

Wyświetl plik

@ -0,0 +1,31 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Wyświetl plik

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

Wyświetl plik

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
tools:context=".MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="To use, enter the command:" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:fontFamily="monospace"
android:text="adb forward tcp:5000 tcp:5000" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Then go to localhost:5000 in your browser." />
</LinearLayout>

Wyświetl plik

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:clipToPadding="false"
android:clipChildren="false">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="5dp"
app:cardCornerRadius="5dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:fontFamily="monospace"
tools:text="Spider-Man"/>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.2 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.3 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.8 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 7.3 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 7.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 12 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 10 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 16 KiB

Wyświetl plik

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.PagingTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

Wyświetl plik

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

Wyświetl plik

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Spinner Test</string>
</resources>

Wyświetl plik

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.PagingTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

Wyświetl plik

@ -0,0 +1,42 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'org.jlleitschuh.gradle.ktlint'
}
android {
buildToolsVersion BUILD_TOOL_VERSION
compileSdkVersion COMPILE_SDK
defaultConfig {
minSdkVersion MINIMUM_SDK
targetSdkVersion TARGET_SDK
}
compileOptions {
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
}
kotlinOptions {
jvmTarget = '1.8'
}
}
ktlint {
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
version = "0.43.2"
}
dependencies {
// Can't use the newest version because it hits some weird NoClassDefFoundException
implementation 'com.github.jknack:handlebars:4.0.7'
implementation libs.androidx.appcompat
implementation libs.material.material
implementation libs.androidx.sqlite
implementation project(':core-util')
testImplementation testLibs.junit.junit
implementation 'org.nanohttpd:nanohttpd-webserver:2.3.1'
}

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="org.signal.spinner">
</manifest>

Wyświetl plik

@ -0,0 +1,47 @@
<html>
{{> head title="Browse" }}
<body>
{{> prefix isBrowse=true}}
<!-- Table Selector -->
<form action="browse" method="post">
<select name="table">
{{#each tableNames}}
<option value="{{this}}">{{this}}</option>
{{/each}}
</select>
<input type="hidden" name="db" value="{{database}}" />
<input type="submit" value="browse" />
</form>
<!-- Data -->
{{#if table}}
<h1>{{table}}</h1>
{{else}}
<h1>Data</h1>
{{/if}}
{{#if queryResult}}
<p>{{queryResult.rowCount}} row(s).</p>
<table>
<tr>
{{#each queryResult.columns}}
<th>{{this}}</th>
{{/each}}
</tr>
{{#each queryResult.rows}}
<tr>
{{#each this}}
<td>{{this}}</td>
{{/each}}
</tr>
{{/each}}
</table>
{{else}}
Select a table from above and click 'browse'.
{{/if}}
{{> suffix}}
</body>
</html>

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,86 @@
<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">
<style type="text/css">
html, body {
font-family: monospace;
}
table, th, td {
border: 1px solid black;
}
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: 12px;
}
.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,52 @@
<html>
{{> head title="Home" }}
<style type="text/css">
h2 {
font-size: 1.25rem;
}
</style>
<body>
{{> prefix isOverview=true}}
<h1 class="collapse-header active" data-for="table-creates">Tables</h1>
<div id="table-creates">
{{#if tables}}
{{#each tables}}
<h2 class="collapse-header" data-for="table-create-{{@index}}">{{name}}</h2>
<div id="table-create-{{@index}}" class="hidden">{{{sql}}}</div>
{{/each}}
{{else}}
None.
{{/if}}
</div>
<h1 class="collapse-header active" data-for="index-creates">Indices</h1>
<div id="index-creates">
{{#if indices}}
{{#each indices}}
<h2 class="collapse-header active" data-for="index-create-{{@index}}">{{name}}</h2>
<div id="index-create-{{@index}}">{{{sql}}}</div>
{{/each}}
{{else}}
None.
{{/if}}
</div>
<h1 class="collapse-header active" data-for="trigger-creates">Triggers</h1>
<div id="trigger-creates">
{{#if triggers}}
{{#each triggers}}
<h2 class="collapse-header active" data-for="trigger-create-{{@index}}">{{name}}</h2>
<div id="trigger-create-{{@index}}">{{{sql}}}</div>
{{/each}}
{{else}}
None.
{{/if}}
</div>
{{> suffix }}
</body>
</html>

Wyświetl plik

@ -0,0 +1,31 @@
<h1>SPINNER</h1>
<table class="device-info">
<tr>
<td>Device</td>
<td>{{deviceInfo.name}}</td>
</tr>
<tr>
<td>Package</td>
<td>{{deviceInfo.packageName}}</td>
</tr>
<tr>
<td>App Version</td>
<td>{{deviceInfo.appVersion}}</td>
</tr>
</table>
<div>
Database:
<select id="database-selector">
{{#each databases}}
<option value="{{this}}" {{eq database this yes="selected" no=""}}>{{this}}</option>
{{/each}}
</select>
</div>
<ol class="tabs">
<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>
</ol>

Wyświetl plik

@ -0,0 +1,43 @@
<html>
{{> head title="Query" }}
<body>
{{> prefix isQuery=true}}
<!-- Query Input -->
<form action="query" method="post">
<textarea name="query" class="query-input" placeholder="Enter your query...">{{query}}</textarea>
<input type="hidden" name="db" value="{{database}}" />
<input type="submit" name="action" value="run" />
or
<input type="submit" name="action" value="analyze" />
</form>
<!-- Query Result -->
<h1>Data</h1>
{{#if queryResult}}
{{queryResult.rowCount}} row(s). <br />
{{queryResult.timeToFirstRow}} ms to read the first row. <br />
{{queryResult.timeToReadRows}} ms to read the rest of the rows. <br />
<br />
<table>
<tr>
{{#each queryResult.columns}}
<th>{{this}}</th>
{{/each}}
</tr>
{{#each queryResult.rows}}
<tr>
{{#each this}}
<td>{{this}}</td>
{{/each}}
</tr>
{{/each}}
</table>
{{else}}
No data.
{{/if}}
{{> suffix}}
</body>
</html>

Wyświetl plik

@ -0,0 +1,17 @@
<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

@ -0,0 +1,47 @@
package org.signal.spinner
import android.content.Context
import com.github.jknack.handlebars.io.StringTemplateSource
import com.github.jknack.handlebars.io.TemplateLoader
import com.github.jknack.handlebars.io.TemplateSource
import org.signal.core.util.StreamUtil
import java.nio.charset.Charset
/**
* A loader read handlebars templates from the assets directory.
*/
class AssetTemplateLoader(private val context: Context) : TemplateLoader {
override fun sourceAt(location: String): TemplateSource {
val content: String = StreamUtil.readFullyAsString(context.assets.open("$location.hbs"))
return StringTemplateSource(location, content)
}
override fun resolve(location: String): String {
return location
}
override fun getPrefix(): String {
return ""
}
override fun getSuffix(): String {
return ""
}
override fun setPrefix(prefix: String) {
TODO("Not yet implemented")
}
override fun setSuffix(suffix: String) {
TODO("Not yet implemented")
}
override fun setCharset(charset: Charset?) {
TODO("Not yet implemented")
}
override fun getCharset(): Charset {
return Charset.defaultCharset()
}
}

Wyświetl plik

@ -0,0 +1,27 @@
package org.signal.spinner
import android.database.Cursor
import androidx.sqlite.db.SupportSQLiteDatabase
fun SupportSQLiteDatabase.getTableNames(): List<String> {
val out = mutableListOf<String>()
this.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC").use { cursor ->
while (cursor.moveToNext()) {
out += cursor.getString(0)
}
}
return out
}
fun SupportSQLiteDatabase.getTables(): Cursor {
return this.query("SELECT * FROM sqlite_master WHERE type='table' ORDER BY name ASC")
}
fun SupportSQLiteDatabase.getIndexes(): Cursor {
return this.query("SELECT * FROM sqlite_master WHERE type='index' ORDER BY name ASC")
}
fun SupportSQLiteDatabase.getTriggers(): Cursor {
return this.query("SELECT * FROM sqlite_master WHERE type='trigger' ORDER BY name ASC")
}

Wyświetl plik

@ -0,0 +1,27 @@
package org.signal.spinner
import android.content.Context
import androidx.sqlite.db.SupportSQLiteDatabase
import org.signal.core.util.logging.Log
import java.io.IOException
/**
* A class to help initialize Spinner, our database debugging interface.
*/
object Spinner {
val TAG: String = Log.tag(Spinner::class.java)
fun init(context: Context, deviceInfo: DeviceInfo, databases: Map<String, SupportSQLiteDatabase>) {
try {
SpinnerServer(context, deviceInfo, databases).start()
} catch (e: IOException) {
Log.w(TAG, "Spinner server hit IO exception! Restarting.", e)
}
}
data class DeviceInfo(
val name: String,
val packageName: String,
val appVersion: String
)
}

Wyświetl plik

@ -0,0 +1,311 @@
package org.signal.spinner
import android.content.Context
import android.database.Cursor
import android.util.Base64
import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.jknack.handlebars.Handlebars
import com.github.jknack.handlebars.Template
import com.github.jknack.handlebars.helper.ConditionalHelpers
import fi.iki.elonen.NanoHTTPD
import org.signal.core.util.ExceptionUtil
import org.signal.core.util.logging.Log
import java.lang.IllegalArgumentException
import kotlin.math.max
/**
* The workhorse of this lib. Handles all of our our web routing and response generation.
*
* In general, you add routes in [serve], and then build a response by creating a handlebars template (in the assets folder) and then passing in a data class
* to [renderTemplate].
*/
internal class SpinnerServer(
context: Context,
private val deviceInfo: Spinner.DeviceInfo,
private val databases: Map<String, SupportSQLiteDatabase>
) : NanoHTTPD(5000) {
companion object {
private val TAG = Log.tag(SpinnerServer::class.java)
}
private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(context)).apply {
registerHelper("eq", ConditionalHelpers.eq)
}
override fun serve(session: IHTTPSession): Response {
if (session.method == Method.POST) {
// Needed to populate session.parameters
session.parseBody(mutableMapOf())
}
val dbParam: String = session.queryParam("db") ?: session.parameters["db"]?.toString() ?: databases.keys.first()
val db: SupportSQLiteDatabase = databases[dbParam] ?: return internalError(IllegalArgumentException("Invalid db param!"))
try {
return when {
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)
else -> newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "Not found")
}
} catch (t: Throwable) {
Log.e(TAG, t)
return internalError(t)
}
}
private fun getIndex(dbName: String, db: SupportSQLiteDatabase): Response {
return renderTemplate(
"overview",
OverviewPageModel(
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
tables = db.getTables().toTableInfo(),
indices = db.getIndexes().toIndexInfo(),
triggers = db.getTriggers().toTriggerInfo(),
queryResult = db.getTables().toQueryResult()
)
)
}
private fun getBrowse(dbName: String, db: SupportSQLiteDatabase): Response {
return renderTemplate(
"browse",
BrowsePageModel(
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
tableNames = db.getTableNames()
)
)
}
private fun postBrowse(dbName: String, db: SupportSQLiteDatabase, session: IHTTPSession): Response {
val table: String = session.parameters["table"]?.get(0).toString()
val query = "select * from $table"
return renderTemplate(
"browse",
BrowsePageModel(
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
tableNames = db.getTableNames(),
table = table,
queryResult = db.query(query).toQueryResult()
)
)
}
private fun getQuery(dbName: String, db: SupportSQLiteDatabase): Response {
return renderTemplate(
"query",
QueryPageModel(
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
query = ""
)
)
}
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()
val query = if (action == "analyze") "EXPLAIN QUERY PLAN $rawQuery" else rawQuery
val startTime = System.currentTimeMillis()
return renderTemplate(
"query",
QueryPageModel(
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
query = rawQuery,
queryResult = db.query(query).toQueryResult(startTime)
)
)
}
private fun internalError(throwable: Throwable): Response {
val stackTrace = ExceptionUtil.convertThrowableToString(throwable)
.split("\n")
.map { it.trim() }
.mapIndexed { index, s -> if (index == 0) s else "&nbsp;&nbsp;$s" }
.joinToString("<br />")
return renderTemplate("error", stackTrace)
}
private fun renderTemplate(assetName: String, model: Any): Response {
val template: Template = handlebars.compile(assetName)
val output: String = template.apply(model)
return newFixedLengthResponse(output)
}
private fun Cursor.toQueryResult(queryStartTime: Long = 0): QueryResult {
val numColumns = this.columnCount
val columns = mutableListOf<String>()
for (i in 0 until numColumns) {
columns += getColumnName(i)
}
var timeOfFirstRow = 0L
val rows = mutableListOf<List<String>>()
while (moveToNext()) {
if (timeOfFirstRow == 0L) {
timeOfFirstRow = System.currentTimeMillis()
}
val row = mutableListOf<String>()
for (i in 0 until numColumns) {
val data: String? = when (getType(i)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(getBlob(i), 0)
else -> getString(i)
}
row += data ?: "null"
}
rows += row
}
if (timeOfFirstRow == 0L) {
timeOfFirstRow = System.currentTimeMillis()
}
return QueryResult(
columns = columns,
rows = rows,
timeToFirstRow = max(timeOfFirstRow - queryStartTime, 0),
timeToReadRows = max(System.currentTimeMillis() - timeOfFirstRow, 0)
)
}
private fun Cursor.toTableInfo(): List<TableInfo> {
val tables = mutableListOf<TableInfo>()
while (moveToNext()) {
val name = getString(getColumnIndexOrThrow("name"))
tables += TableInfo(
name = name ?: "null",
sql = getString(getColumnIndexOrThrow("sql"))?.formatAsSqlCreationStatement(name) ?: "null"
)
}
return tables
}
private fun Cursor.toIndexInfo(): List<IndexInfo> {
val indices = mutableListOf<IndexInfo>()
while (moveToNext()) {
indices += IndexInfo(
name = getString(getColumnIndexOrThrow("name")) ?: "null",
sql = getString(getColumnIndexOrThrow("sql")) ?: "null"
)
}
return indices
}
private fun Cursor.toTriggerInfo(): List<TriggerInfo> {
val indices = mutableListOf<TriggerInfo>()
while (moveToNext()) {
indices += TriggerInfo(
name = getString(getColumnIndexOrThrow("name")) ?: "null",
sql = getString(getColumnIndexOrThrow("sql")) ?: "null"
)
}
return indices
}
/** Takes a SQL table creation statement and formats it using HTML */
private fun String.formatAsSqlCreationStatement(name: String): String {
val fields = substring(indexOf("(") + 1, this.length - 1).split(",")
val fieldStrings = fields.map { s -> "&nbsp;&nbsp;${s.trim()},<br>" }.toMutableList()
if (fieldStrings.isNotEmpty()) {
fieldStrings[fieldStrings.lastIndex] = "&nbsp;&nbsp;${fields.last().trim()}<br>"
}
return "CREATE TABLE $name (<br/>" +
fieldStrings.joinToString("") +
")"
}
private fun IHTTPSession.queryParam(name: String): String? {
if (queryParameterString == null) {
return null
}
val params: Map<String, String> = queryParameterString
.split("&")
.mapNotNull { part ->
val parts = part.split("=")
if (parts.size == 2) {
parts[0] to parts[1]
} else {
null
}
}
.toMap()
return params[name]
}
data class OverviewPageModel(
val deviceInfo: Spinner.DeviceInfo,
val database: String,
val databases: List<String>,
val tables: List<TableInfo>,
val indices: List<IndexInfo>,
val triggers: List<TriggerInfo>,
val queryResult: QueryResult? = null
)
data class BrowsePageModel(
val deviceInfo: Spinner.DeviceInfo,
val database: String,
val databases: List<String>,
val tableNames: List<String>,
val table: String? = null,
val queryResult: QueryResult? = null
)
data class QueryPageModel(
val deviceInfo: Spinner.DeviceInfo,
val database: String,
val databases: List<String>,
val query: String = "",
val queryResult: QueryResult? = null
)
data class QueryResult(
val columns: List<String>,
val rows: List<List<String>>,
val rowCount: Int = rows.size,
val timeToFirstRow: Long,
val timeToReadRows: Long,
)
data class TableInfo(
val name: String,
val sql: String
)
data class IndexInfo(
val name: String,
val sql: String
)
data class TriggerInfo(
val name: String,
val sql: String
)
}