Replace Flipper with Spinner.
7
.idea/codeStyles/Project.xml
generated
|
@ -51,6 +51,13 @@
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
</JetCodeStyleSettings>
|
</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">
|
<codeStyleSettings language="JAVA">
|
||||||
<option name="BRACE_STYLE" value="5" />
|
<option name="BRACE_STYLE" value="5" />
|
||||||
<option name="CLASS_BRACE_STYLE" value="5" />
|
<option name="CLASS_BRACE_STYLE" value="5" />
|
||||||
|
|
|
@ -75,18 +75,18 @@ def abiPostFix = ['universal' : 0,
|
||||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||||
|
|
||||||
def selectableVariants = [
|
def selectableVariants = [
|
||||||
'nightlyProdFlipper',
|
'nightlyProdSpinner',
|
||||||
'nightlyProdPerf',
|
'nightlyProdPerf',
|
||||||
'nightlyProdRelease',
|
'nightlyProdRelease',
|
||||||
'playProdDebug',
|
'playProdDebug',
|
||||||
'playProdFlipper',
|
'playProdSpinner',
|
||||||
'playProdPerf',
|
'playProdPerf',
|
||||||
'playProdRelease',
|
'playProdRelease',
|
||||||
'playStagingDebug',
|
'playStagingDebug',
|
||||||
'playStagingFlipper',
|
'playStagingSpinner',
|
||||||
'playStagingPerf',
|
'playStagingPerf',
|
||||||
'playStagingRelease',
|
'playStagingRelease',
|
||||||
'websiteProdFlipper',
|
'websiteProdSpinner',
|
||||||
'websiteProdRelease',
|
'websiteProdRelease',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -250,12 +250,12 @@ android {
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||||
}
|
}
|
||||||
flipper {
|
spinner {
|
||||||
initWith debug
|
initWith debug
|
||||||
isDefault false
|
isDefault false
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
matchingFallbacks = ['debug']
|
matchingFallbacks = ['debug']
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
|
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
|
@ -509,9 +509,8 @@ dependencies {
|
||||||
}
|
}
|
||||||
implementation libs.dnsjava
|
implementation libs.dnsjava
|
||||||
|
|
||||||
flipperImplementation libs.facebook.flipper
|
spinnerImplementation project(":spinner")
|
||||||
flipperImplementation libs.facebook.soloader
|
spinnerImplementation libs.square.leakcanary
|
||||||
flipperImplementation libs.square.leakcanary
|
|
||||||
|
|
||||||
testImplementation testLibs.junit.junit
|
testImplementation testLibs.junit.junit
|
||||||
testImplementation testLibs.assertj.core
|
testImplementation testLibs.assertj.core
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@
|
||||||
package="org.thoughtcrime.securesms">
|
package="org.thoughtcrime.securesms">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".FlipperApplicationContext"
|
android:name=".SpinnerApplicationContext"
|
||||||
tools:replace="android:name">
|
tools:replace="android:name">
|
||||||
|
|
||||||
<activity
|
<activity
|
|
@ -1,25 +1,37 @@
|
||||||
package org.thoughtcrime.securesms
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
import com.facebook.flipper.android.AndroidFlipperClient
|
import android.os.Build
|
||||||
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 leakcanary.LeakCanary
|
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
|
import shark.AndroidReferenceMatchers
|
||||||
|
|
||||||
class FlipperApplicationContext : ApplicationContext() {
|
class SpinnerApplicationContext : ApplicationContext() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
SoLoader.init(this, false)
|
|
||||||
|
|
||||||
val client = AndroidFlipperClient.getInstance(this)
|
Spinner.init(
|
||||||
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
this,
|
||||||
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
|
Spinner.DeviceInfo(
|
||||||
client.addPlugin(SharedPreferencesFlipperPlugin(this))
|
name = "${Build.MODEL} (Android ${Build.VERSION.RELEASE}, API ${Build.VERSION.SDK_INT})",
|
||||||
client.start()
|
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(
|
LeakCanary.config = LeakCanary.config.copy(
|
||||||
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
|
@ -115,8 +115,6 @@ dependencyResolutionManagement {
|
||||||
alias('stickyheadergrid').to('com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4')
|
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('circular-progress-button').to('com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2')
|
||||||
alias('dnsjava').to('dnsjava:dnsjava:2.1.9')
|
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
|
// Mp4Parser
|
||||||
alias('mp4parser-isoparser').to('org.mp4parser', 'isoparser').versionRef('mp4parser')
|
alias('mp4parser-isoparser').to('org.mp4parser', 'isoparser').versionRef('mp4parser')
|
||||||
|
|
|
@ -23,6 +23,14 @@
|
||||||
<sha256 value="b7730754793e2fa510ddb10b7514e65f8706e4ec4b100acf7e4215f0bd5519b4" origin="Generated by Gradle"/>
|
<sha256 value="b7730754793e2fa510ddb10b7514e65f8706e4ec4b100acf7e4215f0bd5519b4" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.activity" name="activity-ktx" version="1.2.2">
|
||||||
<artifact name="activity-ktx-1.2.2.aar">
|
<artifact name="activity-ktx-1.2.2.aar">
|
||||||
<sha256 value="9829e13d6a6b045b03b21a330512e091dc76eb5b3ded0d88d1ab0509cf84a50e" origin="Generated by Gradle"/>
|
<sha256 value="9829e13d6a6b045b03b21a330512e091dc76eb5b3ded0d88d1ab0509cf84a50e" origin="Generated by Gradle"/>
|
||||||
|
@ -31,6 +39,14 @@
|
||||||
<sha256 value="92f4431091650b5a67cc4f654bd9b822c585cf4262180912f075779f07a04ba6" origin="Generated by Gradle"/>
|
<sha256 value="92f4431091650b5a67cc4f654bd9b822c585cf4262180912f075779f07a04ba6" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.annotation" name="annotation" version="1.0.0">
|
||||||
<artifact name="annotation-1.0.0.jar">
|
<artifact name="annotation-1.0.0.jar">
|
||||||
<sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle"/>
|
<sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle"/>
|
||||||
|
@ -54,6 +70,14 @@
|
||||||
<sha256 value="b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11" origin="Generated by Gradle"/>
|
<sha256 value="b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.appcompat" name="appcompat" version="1.2.0">
|
||||||
<artifact name="appcompat-1.2.0.aar">
|
<artifact name="appcompat-1.2.0.aar">
|
||||||
<sha256 value="3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70" origin="Generated by Gradle"/>
|
<sha256 value="3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70" origin="Generated by Gradle"/>
|
||||||
|
@ -213,6 +237,14 @@
|
||||||
<sha256 value="e3877fa529fe29177f34a26e0790ed35544848b0c7503bfed30b2539f1686d65" origin="Generated by Gradle"/>
|
<sha256 value="e3877fa529fe29177f34a26e0790ed35544848b0c7503bfed30b2539f1686d65" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.core" name="core-ktx" version="1.5.0">
|
||||||
<artifact name="core-ktx-1.5.0.aar">
|
<artifact name="core-ktx-1.5.0.aar">
|
||||||
<sha256 value="5964cfe7a4882da2a00fb6ca3d3a072d04139208186f7bc4b3cb66022764fc42" origin="Generated by Gradle"/>
|
<sha256 value="5964cfe7a4882da2a00fb6ca3d3a072d04139208186f7bc4b3cb66022764fc42" origin="Generated by Gradle"/>
|
||||||
|
@ -1507,6 +1539,21 @@
|
||||||
<sha256 value="23f5c982e1c7771423d37d52c774e8d2e80fd7ea7305ebe448797a96f67e6fca" origin="Generated by Gradle"/>
|
<sha256 value="23f5c982e1c7771423d37d52c774e8d2e80fd7ea7305ebe448797a96f67e6fca" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="com.github.shyiko.klob" name="klob" version="0.2.1">
|
||||||
<artifact name="klob-0.2.1.jar">
|
<artifact name="klob-0.2.1.jar">
|
||||||
<sha256 value="2f6174e3049008f263fd832813390df645ac5c7cfa79f170ace58690810476f2" origin="Generated by Gradle"/>
|
<sha256 value="2f6174e3049008f263fd832813390df645ac5c7cfa79f170ace58690810476f2" origin="Generated by Gradle"/>
|
||||||
|
@ -1810,6 +1857,11 @@
|
||||||
<sha256 value="e6dd072f9d3fe02a4600688380bd422bdac184caf6fe2418cfdd0934f09432aa" origin="Generated by Gradle"/>
|
<sha256 value="e6dd072f9d3fe02a4600688380bd422bdac184caf6fe2418cfdd0934f09432aa" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<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">
|
<artifact name="listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar">
|
||||||
<sha256 value="b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99" origin="Generated by Gradle"/>
|
<sha256 value="b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99" origin="Generated by Gradle"/>
|
||||||
|
@ -2562,11 +2614,21 @@
|
||||||
<sha256 value="a32de739cfdf515774e696f91aa9697d2e7731e5cb5045ca8a4b657f8b1b4fb4" origin="Generated by Gradle"/>
|
<sha256 value="a32de739cfdf515774e696f91aa9697d2e7731e5cb5045ca8a4b657f8b1b4fb4" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.antlr" name="antlr4-runtime" version="4.5.2-1">
|
||||||
<artifact name="antlr4-runtime-4.5.2-1.jar">
|
<artifact name="antlr4-runtime-4.5.2-1.jar">
|
||||||
<sha256 value="e831413004bceed7d915c3a175927b1daabc4974b7b8a6f87bbce886d3550398" origin="Generated by Gradle"/>
|
<sha256 value="e831413004bceed7d915c3a175927b1daabc4974b7b8a6f87bbce886d3550398" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.apache.ant" name="ant" version="1.10.9">
|
||||||
<artifact name="ant-1.10.9.jar">
|
<artifact name="ant-1.10.9.jar">
|
||||||
<sha256 value="0715478af585ea80a18985613ebecdc7922122d45b2c3c970ff9b352cddb75fc" origin="Generated by Gradle"/>
|
<sha256 value="0715478af585ea80a18985613ebecdc7922122d45b2c3c970ff9b352cddb75fc" origin="Generated by Gradle"/>
|
||||||
|
@ -2597,6 +2659,11 @@
|
||||||
<sha256 value="0aeb625c948c697ea7b205156e112363b59ed5e2551212cd4e460bdb72c7c06e" origin="Generated by Gradle"/>
|
<sha256 value="0aeb625c948c697ea7b205156e112363b59ed5e2551212cd4e460bdb72c7c06e" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.apache.httpcomponents" name="httpclient" version="4.5.6">
|
||||||
<artifact name="httpclient-4.5.6.jar">
|
<artifact name="httpclient-4.5.6.jar">
|
||||||
<sha256 value="c03f813195e7a80e3608d0ddd8da80b21696a4c92a6a2298865bf149071551c7" origin="Generated by Gradle"/>
|
<sha256 value="c03f813195e7a80e3608d0ddd8da80b21696a4c92a6a2298865bf149071551c7" origin="Generated by Gradle"/>
|
||||||
|
@ -3192,6 +3259,14 @@
|
||||||
<sha256 value="4a80f7a521f70a87798e74416b596336c76d8306594172a4cf142c16e1720081" origin="Generated by Gradle"/>
|
<sha256 value="4a80f7a521f70a87798e74416b596336c76d8306594172a4cf142c16e1720081" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.5.0">
|
||||||
<artifact name="kotlinx-coroutines-android-1.5.0.jar">
|
<artifact name="kotlinx-coroutines-android-1.5.0.jar">
|
||||||
<sha256 value="7099198391d673c199fea084423d9f3fdc79470acba19111330c7f88504279c7" origin="Generated by Gradle"/>
|
<sha256 value="7099198391d673c199fea084423d9f3fdc79470acba19111330c7f88504279c7" origin="Generated by Gradle"/>
|
||||||
|
@ -3205,6 +3280,11 @@
|
||||||
<sha256 value="f8c8b7485d4a575e38e5e94945539d1d4eccd3228a199e1a9aa094e8c26174ee" origin="Generated by Gradle"/>
|
<sha256 value="f8c8b7485d4a575e38e5e94945539d1d4eccd3228a199e1a9aa094e8c26174ee" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.5.0">
|
||||||
<artifact name="kotlinx-coroutines-core-1.5.0.jar">
|
<artifact name="kotlinx-coroutines-core-1.5.0.jar">
|
||||||
<sha256 value="6f738012913d3d4bc18408a5011108d4744a72b6233662ee4d4dd50da9550b8d" origin="Generated by Gradle"/>
|
<sha256 value="6f738012913d3d4bc18408a5011108d4744a72b6233662ee4d4dd50da9550b8d" origin="Generated by Gradle"/>
|
||||||
|
@ -3213,6 +3293,14 @@
|
||||||
<sha256 value="d8a26a896da32fb1f8c3f13fe41cb798a612a1c1ddf3a555d82ee1ff16ef13d3" origin="Generated by Gradle"/>
|
<sha256 value="d8a26a896da32fb1f8c3f13fe41cb798a612a1c1ddf3a555d82ee1ff16ef13d3" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.5.0">
|
||||||
<artifact name="kotlinx-coroutines-core-jvm-1.5.0.jar">
|
<artifact name="kotlinx-coroutines-core-jvm-1.5.0.jar">
|
||||||
<sha256 value="78d6cc7135f84d692ff3752fcfd1fa1bbe0940d7df70652e4f1eaeec0c78afbb" origin="Generated by Gradle"/>
|
<sha256 value="78d6cc7135f84d692ff3752fcfd1fa1bbe0940d7df70652e4f1eaeec0c78afbb" origin="Generated by Gradle"/>
|
||||||
|
@ -3221,6 +3309,14 @@
|
||||||
<sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/>
|
<sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.jetbrains.kotlinx" name="kotlinx-serialization-core" version="1.1.0">
|
||||||
<artifact name="kotlinx-serialization-core-1.1.0.module">
|
<artifact name="kotlinx-serialization-core-1.1.0.module">
|
||||||
<sha256 value="a21890616c068b55580ca3cf008b3d5d7f9613c980b754b4ad5a5bf74e8babf5" origin="Generated by Gradle"/>
|
<sha256 value="a21890616c068b55580ca3cf008b3d5d7f9613c980b754b4ad5a5bf74e8babf5" origin="Generated by Gradle"/>
|
||||||
|
@ -3340,6 +3436,16 @@
|
||||||
<sha256 value="4be648c50456fba4686ba825000d628c1d805a3b92272ba9ad5b697dfa43036b" origin="Generated by Gradle"/>
|
<sha256 value="4be648c50456fba4686ba825000d628c1d805a3b92272ba9ad5b697dfa43036b" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.mp4parser" name="isoparser" version="1.9.39">
|
||||||
<artifact name="isoparser-1.9.39.jar">
|
<artifact name="isoparser-1.9.39.jar">
|
||||||
<sha256 value="a3a7172648f1ac4b2a369ecca2861317e472179c842a5217b08643ba0a1dfa12" origin="Generated by Gradle"/>
|
<sha256 value="a3a7172648f1ac4b2a369ecca2861317e472179c842a5217b08643ba0a1dfa12" origin="Generated by Gradle"/>
|
||||||
|
@ -3355,6 +3461,16 @@
|
||||||
<sha256 value="da5151cfc3bf491d550fb9127bba22736f4b7416058d58a1a5fcfdfa3673876d" origin="Generated by Gradle"/>
|
<sha256 value="da5151cfc3bf491d550fb9127bba22736f4b7416058d58a1a5fcfdfa3673876d" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.objenesis" name="objenesis" version="2.6">
|
||||||
<artifact name="objenesis-2.6.jar">
|
<artifact name="objenesis-2.6.jar">
|
||||||
<sha256 value="5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d" origin="Generated by Gradle"/>
|
<sha256 value="5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d" origin="Generated by Gradle"/>
|
||||||
|
@ -3550,11 +3666,21 @@
|
||||||
<sha256 value="fc0e57dec3836f2560b8d26dae8d2e330052b920b3028b49dfbd9c6b9a8ed0e2" origin="Generated by Gradle"/>
|
<sha256 value="fc0e57dec3836f2560b8d26dae8d2e330052b920b3028b49dfbd9c6b9a8ed0e2" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.slf4j" name="slf4j-api" version="1.7.24">
|
||||||
<artifact name="slf4j-api-1.7.24.jar">
|
<artifact name="slf4j-api-1.7.24.jar">
|
||||||
<sha256 value="baf3c7fe15fefeaf9e5b000d94547379dc48370f22a8797e239c127e7d7756ec" origin="Generated by Gradle"/>
|
<sha256 value="baf3c7fe15fefeaf9e5b000d94547379dc48370f22a8797e239c127e7d7756ec" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="org.smali" name="dexlib2" version="2.2.4">
|
||||||
<artifact name="dexlib2-2.2.4.jar">
|
<artifact name="dexlib2-2.2.4.jar">
|
||||||
<sha256 value="cb2677bfb66cfbc954e96e806ac6bda13051ad37754f9d1bcce38514e50e41e6" origin="Generated by Gradle"/>
|
<sha256 value="cb2677bfb66cfbc954e96e806ac6bda13051ad37754f9d1bcce38514e50e41e6" origin="Generated by Gradle"/>
|
||||||
|
|
|
@ -14,6 +14,8 @@ include ':image-editor'
|
||||||
include ':image-editor-app'
|
include ':image-editor-app'
|
||||||
include ':donations'
|
include ':donations'
|
||||||
include ':donations-app'
|
include ':donations-app'
|
||||||
|
include ':spinner'
|
||||||
|
include ':spinner-app'
|
||||||
|
|
||||||
project(':app').name = 'Signal-Android'
|
project(':app').name = 'Signal-Android'
|
||||||
project(':paging').projectDir = file('paging/lib')
|
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').projectDir = file('donations/lib')
|
||||||
project(':donations-app').projectDir = file('donations/app')
|
project(':donations-app').projectDir = file('donations/app')
|
||||||
|
|
||||||
|
project(':spinner').projectDir = file('spinner/lib')
|
||||||
|
project(':spinner-app').projectDir = file('spinner/app')
|
||||||
|
|
||||||
rootProject.name='Signal'
|
rootProject.name='Signal'
|
||||||
|
|
||||||
apply from: 'dependencies.gradle'
|
apply from: 'dependencies.gradle'
|
||||||
|
|
21
spinner/README.md
Normal file
|
@ -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).
|
61
spinner/app/build.gradle
Normal file
|
@ -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')
|
||||||
|
}
|
24
spinner/app/src/main/AndroidManifest.xml
Normal file
|
@ -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>
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
171
spinner/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -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>
|
30
spinner/app/src/main/res/layout/activity_main.xml
Normal file
|
@ -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>
|
30
spinner/app/src/main/res/layout/item.xml
Normal file
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
BIN
spinner/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
spinner/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
spinner/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
spinner/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
spinner/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
spinner/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
spinner/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
spinner/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
spinner/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
spinner/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 16 KiB |
16
spinner/app/src/main/res/values-night/themes.xml
Normal file
|
@ -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>
|
10
spinner/app/src/main/res/values/colors.xml
Normal file
|
@ -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>
|
3
spinner/app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Spinner Test</string>
|
||||||
|
</resources>
|
16
spinner/app/src/main/res/values/themes.xml
Normal file
|
@ -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>
|
42
spinner/lib/build.gradle
Normal file
|
@ -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'
|
||||||
|
}
|
6
spinner/lib/src/main/AndroidManifest.xml
Normal file
|
@ -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>
|
47
spinner/lib/src/main/assets/browse.hbs
Normal file
|
@ -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>
|
8
spinner/lib/src/main/assets/error.hbs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
{{> head title="Error :(" }}
|
||||||
|
<body>
|
||||||
|
Hit an exception while trying to serve the page :(
|
||||||
|
<hr/>
|
||||||
|
{{{this}}}
|
||||||
|
</body>
|
||||||
|
</html>
|
86
spinner/lib/src/main/assets/head.hbs
Normal file
|
@ -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>
|
52
spinner/lib/src/main/assets/overview.hbs
Normal file
|
@ -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>
|
31
spinner/lib/src/main/assets/prefix.hbs
Normal file
|
@ -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>
|
43
spinner/lib/src/main/assets/query.hbs
Normal file
|
@ -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>
|
17
spinner/lib/src/main/assets/suffix.hbs
Normal file
|
@ -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>
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
27
spinner/lib/src/main/java/org/signal/spinner/DatabaseUtil.kt
Normal file
|
@ -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")
|
||||||
|
}
|
27
spinner/lib/src/main/java/org/signal/spinner/Spinner.kt
Normal file
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
311
spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt
Normal file
|
@ -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 " $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 -> " ${s.trim()},<br>" }.toMutableList()
|
||||||
|
|
||||||
|
if (fieldStrings.isNotEmpty()) {
|
||||||
|
fieldStrings[fieldStrings.lastIndex] = " ${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
|
||||||
|
)
|
||||||
|
}
|