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_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JAVA">
|
||||
<option name="BRACE_STYLE" value="5" />
|
||||
<option name="CLASS_BRACE_STYLE" value="5" />
|
||||
|
|
|
@ -75,18 +75,18 @@ def abiPostFix = ['universal' : 0,
|
|||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'nightlyProdFlipper',
|
||||
'nightlyProdSpinner',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'playProdDebug',
|
||||
'playProdFlipper',
|
||||
'playProdSpinner',
|
||||
'playProdPerf',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingFlipper',
|
||||
'playStagingSpinner',
|
||||
'playStagingPerf',
|
||||
'playStagingRelease',
|
||||
'websiteProdFlipper',
|
||||
'websiteProdSpinner',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
|
||||
|
@ -250,12 +250,12 @@ android {
|
|||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
flipper {
|
||||
spinner {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
|
@ -509,9 +509,8 @@ dependencies {
|
|||
}
|
||||
implementation libs.dnsjava
|
||||
|
||||
flipperImplementation libs.facebook.flipper
|
||||
flipperImplementation libs.facebook.soloader
|
||||
flipperImplementation libs.square.leakcanary
|
||||
spinnerImplementation project(":spinner")
|
||||
spinnerImplementation libs.square.leakcanary
|
||||
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
|
|
|
@ -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">
|
||||
|
||||
<application
|
||||
android:name=".FlipperApplicationContext"
|
||||
android:name=".SpinnerApplicationContext"
|
||||
tools:replace="android:name">
|
||||
|
||||
<activity
|
|
@ -1,25 +1,37 @@
|
|||
package org.thoughtcrime.securesms
|
||||
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import android.os.Build
|
||||
import leakcanary.LeakCanary
|
||||
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
|
||||
import org.signal.spinner.Spinner
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||
import org.thoughtcrime.securesms.database.LogDatabase
|
||||
import org.thoughtcrime.securesms.database.MegaphoneDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.util.AppSignatureUtil
|
||||
import shark.AndroidReferenceMatchers
|
||||
|
||||
class FlipperApplicationContext : ApplicationContext() {
|
||||
class SpinnerApplicationContext : ApplicationContext() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, false)
|
||||
|
||||
val client = AndroidFlipperClient.getInstance(this)
|
||||
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
|
||||
client.addPlugin(SharedPreferencesFlipperPlugin(this))
|
||||
client.start()
|
||||
Spinner.init(
|
||||
this,
|
||||
Spinner.DeviceInfo(
|
||||
name = "${Build.MODEL} (Android ${Build.VERSION.RELEASE}, API ${Build.VERSION.SDK_INT})",
|
||||
packageName = "$packageName (${AppSignatureUtil.getAppSignature(this).or("Unknown")})",
|
||||
appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.CANONICAL_VERSION_CODE}, ${BuildConfig.GIT_HASH})"
|
||||
),
|
||||
linkedMapOf(
|
||||
"signal" to SignalDatabase.rawDatabase,
|
||||
"jobmanager" to JobDatabase.getInstance(this).sqlCipherDatabase,
|
||||
"keyvalue" to KeyValueDatabase.getInstance(this).sqlCipherDatabase,
|
||||
"megaphones" to MegaphoneDatabase.getInstance(this).sqlCipherDatabase,
|
||||
"localmetrics" to LocalMetricsDatabase.getInstance(this).sqlCipherDatabase,
|
||||
"logs" to LogDatabase.getInstance(this).sqlCipherDatabase,
|
||||
)
|
||||
)
|
||||
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
|
@ -115,8 +115,6 @@ dependencyResolutionManagement {
|
|||
alias('stickyheadergrid').to('com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4')
|
||||
alias('circular-progress-button').to('com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2')
|
||||
alias('dnsjava').to('dnsjava:dnsjava:2.1.9')
|
||||
alias('facebook-flipper').to('com.facebook.flipper:flipper:0.91.0')
|
||||
alias('facebook-soloader').to('com.facebook.soloader:soloader:0.10.1')
|
||||
|
||||
// Mp4Parser
|
||||
alias('mp4parser-isoparser').to('org.mp4parser', 'isoparser').versionRef('mp4parser')
|
||||
|
|
|
@ -23,6 +23,14 @@
|
|||
<sha256 value="b7730754793e2fa510ddb10b7514e65f8706e4ec4b100acf7e4215f0bd5519b4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.activity" name="activity" version="1.4.0">
|
||||
<artifact name="activity-1.4.0.aar">
|
||||
<sha256 value="89dc38e0cdbd11f328c7d0b3b021ddb387ca9da0d49f14b18c91e300c45ed79c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="activity-1.4.0.module">
|
||||
<sha256 value="b38ce719cf1862701ab54b48405fc832a8ca8d4aacb2ce0d37456d0aff329147" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.activity" name="activity-ktx" version="1.2.2">
|
||||
<artifact name="activity-ktx-1.2.2.aar">
|
||||
<sha256 value="9829e13d6a6b045b03b21a330512e091dc76eb5b3ded0d88d1ab0509cf84a50e" origin="Generated by Gradle"/>
|
||||
|
@ -31,6 +39,14 @@
|
|||
<sha256 value="92f4431091650b5a67cc4f654bd9b822c585cf4262180912f075779f07a04ba6" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.activity" name="activity-ktx" version="1.4.0">
|
||||
<artifact name="activity-ktx-1.4.0.aar">
|
||||
<sha256 value="3f301941f37a90b4bc553dbbe84e7464a97c0d21df6cf2d6c0cb1b2c07349f33" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="activity-ktx-1.4.0.module">
|
||||
<sha256 value="44950669cc9951b30ca8f9dd426fff3d660672262e74afac785bded4aacc5a03" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.annotation" name="annotation" version="1.0.0">
|
||||
<artifact name="annotation-1.0.0.jar">
|
||||
<sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle"/>
|
||||
|
@ -54,6 +70,14 @@
|
|||
<sha256 value="b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0">
|
||||
<artifact name="annotation-experimental-1.1.0.aar">
|
||||
<sha256 value="0157de61a2064047896a058080f3fd67ba57ad9a94857b3f7a363660243e3f90" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="annotation-experimental-1.1.0.module">
|
||||
<sha256 value="0361d1526a4d7501255e19779e09e93cdbd07fee0e2f5c50b7a137432d510119" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.appcompat" name="appcompat" version="1.2.0">
|
||||
<artifact name="appcompat-1.2.0.aar">
|
||||
<sha256 value="3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70" origin="Generated by Gradle"/>
|
||||
|
@ -213,6 +237,14 @@
|
|||
<sha256 value="e3877fa529fe29177f34a26e0790ed35544848b0c7503bfed30b2539f1686d65" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.core" name="core" version="1.7.0">
|
||||
<artifact name="core-1.7.0.aar">
|
||||
<sha256 value="aaf6734226fff923784f92f65d78a2984dbf17534138855c5ce2038f18656e0b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="core-1.7.0.module">
|
||||
<sha256 value="988f820899d5a4982e5c878ca1cd417970ace332ea2ff72f5be19b233fa0e788" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.core" name="core-ktx" version="1.5.0">
|
||||
<artifact name="core-ktx-1.5.0.aar">
|
||||
<sha256 value="5964cfe7a4882da2a00fb6ca3d3a072d04139208186f7bc4b3cb66022764fc42" origin="Generated by Gradle"/>
|
||||
|
@ -1507,6 +1539,21 @@
|
|||
<sha256 value="23f5c982e1c7771423d37d52c774e8d2e80fd7ea7305ebe448797a96f67e6fca" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.github.jknack" name="handlebars" version="4.0.6">
|
||||
<artifact name="handlebars-4.0.6.jar">
|
||||
<sha256 value="f20c47fd6572170951e83af1c11a5c12e724fa60535d62219bf2f762620d5781" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.github.jknack" name="handlebars" version="4.0.7">
|
||||
<artifact name="handlebars-4.0.7.jar">
|
||||
<sha256 value="d9b155fe8c8ddb0f9b3e5b156b5909dcd4c89d93defb2f72d0961aa633ad838f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.github.jknack" name="handlebars" version="4.3.0">
|
||||
<artifact name="handlebars-4.3.0.jar">
|
||||
<sha256 value="9441bb30635ae1db3a73d793accfe91ed4c2a4edec39750557f11f1debbc8eb7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.github.shyiko.klob" name="klob" version="0.2.1">
|
||||
<artifact name="klob-0.2.1.jar">
|
||||
<sha256 value="2f6174e3049008f263fd832813390df645ac5c7cfa79f170ace58690810476f2" origin="Generated by Gradle"/>
|
||||
|
@ -1810,6 +1857,11 @@
|
|||
<sha256 value="e6dd072f9d3fe02a4600688380bd422bdac184caf6fe2418cfdd0934f09432aa" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.guava" name="listenablefuture" version="1.0">
|
||||
<artifact name="listenablefuture-1.0.jar">
|
||||
<sha256 value="e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.guava" name="listenablefuture" version="9999.0-empty-to-avoid-conflict-with-guava">
|
||||
<artifact name="listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar">
|
||||
<sha256 value="b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99" origin="Generated by Gradle"/>
|
||||
|
@ -2562,11 +2614,21 @@
|
|||
<sha256 value="a32de739cfdf515774e696f91aa9697d2e7731e5cb5045ca8a4b657f8b1b4fb4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.antlr" name="antlr4-runtime" version="4.5.1-1">
|
||||
<artifact name="antlr4-runtime-4.5.1-1.jar">
|
||||
<sha256 value="ffca72bc2a25bb2b0c80a58cee60530a78be17da739bb6c91a8c2e3584ca099e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.antlr" name="antlr4-runtime" version="4.5.2-1">
|
||||
<artifact name="antlr4-runtime-4.5.2-1.jar">
|
||||
<sha256 value="e831413004bceed7d915c3a175927b1daabc4974b7b8a6f87bbce886d3550398" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.antlr" name="antlr4-runtime" version="4.7.1">
|
||||
<artifact name="antlr4-runtime-4.7.1.jar">
|
||||
<sha256 value="43516d19beae35909e04d06af6c0c58c17bc94e0070c85e8dc9929ca640dc91d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.apache.ant" name="ant" version="1.10.9">
|
||||
<artifact name="ant-1.10.9.jar">
|
||||
<sha256 value="0715478af585ea80a18985613ebecdc7922122d45b2c3c970ff9b352cddb75fc" origin="Generated by Gradle"/>
|
||||
|
@ -2597,6 +2659,11 @@
|
|||
<sha256 value="0aeb625c948c697ea7b205156e112363b59ed5e2551212cd4e460bdb72c7c06e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.apache.commons" name="commons-lang3" version="3.1">
|
||||
<artifact name="commons-lang3-3.1.jar">
|
||||
<sha256 value="131f0519a8e4602e47cf024bfd7e0834bcf5592a7207f9a2fdb711d4f5afc166" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.apache.httpcomponents" name="httpclient" version="4.5.6">
|
||||
<artifact name="httpclient-4.5.6.jar">
|
||||
<sha256 value="c03f813195e7a80e3608d0ddd8da80b21696a4c92a6a2298865bf149071551c7" origin="Generated by Gradle"/>
|
||||
|
@ -3192,6 +3259,14 @@
|
|||
<sha256 value="4a80f7a521f70a87798e74416b596336c76d8306594172a4cf142c16e1720081" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.4.1">
|
||||
<artifact name="kotlinx-coroutines-android-1.4.1.jar">
|
||||
<sha256 value="d4cadb673b2101f1ee5fbc147956ac78b1cfd9cc255fb53d3aeb88dff11d99ca" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="kotlinx-coroutines-android-1.4.1.module">
|
||||
<sha256 value="d576909e18f54010f0429aea9e5155a27462a53925bdc4f6fccd96bfccff5145" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.5.0">
|
||||
<artifact name="kotlinx-coroutines-android-1.5.0.jar">
|
||||
<sha256 value="7099198391d673c199fea084423d9f3fdc79470acba19111330c7f88504279c7" origin="Generated by Gradle"/>
|
||||
|
@ -3205,6 +3280,11 @@
|
|||
<sha256 value="f8c8b7485d4a575e38e5e94945539d1d4eccd3228a199e1a9aa094e8c26174ee" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.4.1">
|
||||
<artifact name="kotlinx-coroutines-core-1.4.1.module">
|
||||
<sha256 value="3c00e44941f134b18cadbc5f18ab7b7f23d3ef1f78af95e344cb9c605db21a44" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.5.0">
|
||||
<artifact name="kotlinx-coroutines-core-1.5.0.jar">
|
||||
<sha256 value="6f738012913d3d4bc18408a5011108d4744a72b6233662ee4d4dd50da9550b8d" origin="Generated by Gradle"/>
|
||||
|
@ -3213,6 +3293,14 @@
|
|||
<sha256 value="d8a26a896da32fb1f8c3f13fe41cb798a612a1c1ddf3a555d82ee1ff16ef13d3" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.4.1">
|
||||
<artifact name="kotlinx-coroutines-core-jvm-1.4.1.jar">
|
||||
<sha256 value="6d2f87764b6638f27aff12ed380db4b63c9d46ba55dc32683a650598fa5a3e22" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="kotlinx-coroutines-core-jvm-1.4.1.module">
|
||||
<sha256 value="e6a6f8ffb1a40bc76a935cdd5ecfcf5485b5cd0ec2883044a4760cbfe3315707" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.5.0">
|
||||
<artifact name="kotlinx-coroutines-core-jvm-1.5.0.jar">
|
||||
<sha256 value="78d6cc7135f84d692ff3752fcfd1fa1bbe0940d7df70652e4f1eaeec0c78afbb" origin="Generated by Gradle"/>
|
||||
|
@ -3221,6 +3309,14 @@
|
|||
<sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-metadata" version="1.4.1">
|
||||
<artifact name="kotlinx-coroutines-core-metadata-1.4.1.jar">
|
||||
<sha256 value="22a9e8c5d2e3dbf8e539f1bd0e0516845e6cac0e5708476cc33b1bb994f2b650" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="kotlinx-coroutines-core-metadata-1.4.1.module">
|
||||
<sha256 value="c075dc88140a6ab48a6657113fa0b587ddfa82640a4672247dafc0de208f1192" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-serialization-core" version="1.1.0">
|
||||
<artifact name="kotlinx-serialization-core-1.1.0.module">
|
||||
<sha256 value="a21890616c068b55580ca3cf008b3d5d7f9613c980b754b4ad5a5bf74e8babf5" origin="Generated by Gradle"/>
|
||||
|
@ -3340,6 +3436,16 @@
|
|||
<sha256 value="4be648c50456fba4686ba825000d628c1d805a3b92272ba9ad5b697dfa43036b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.mozilla" name="rhino" version="1.7.7">
|
||||
<artifact name="rhino-1.7.7.jar">
|
||||
<sha256 value="73b8d6bbbd1a6a3a87ea0eea301996deac83f8d40b404518a10afd4d320b5b31" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.mozilla" name="rhino" version="1.7R4">
|
||||
<artifact name="rhino-1.7R4.jar">
|
||||
<sha256 value="eb4cbd05a48ee4448825da229e94115e68adc6c5638d29022914e1178c60a6c4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.mp4parser" name="isoparser" version="1.9.39">
|
||||
<artifact name="isoparser-1.9.39.jar">
|
||||
<sha256 value="a3a7172648f1ac4b2a369ecca2861317e472179c842a5217b08643ba0a1dfa12" origin="Generated by Gradle"/>
|
||||
|
@ -3355,6 +3461,16 @@
|
|||
<sha256 value="da5151cfc3bf491d550fb9127bba22736f4b7416058d58a1a5fcfdfa3673876d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.nanohttpd" name="nanohttpd" version="2.3.1">
|
||||
<artifact name="nanohttpd-2.3.1.jar">
|
||||
<sha256 value="de864c47818157141a24c9acb36df0c47d7bf15b7ff48c90610f3eb4e5df0e58" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.nanohttpd" name="nanohttpd-webserver" version="2.3.1">
|
||||
<artifact name="nanohttpd-webserver-2.3.1.jar">
|
||||
<sha256 value="c2a094648a63d55a9577934c85a79e5ea28b8b138b4915d3494c75aa23ca0ab9" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.objenesis" name="objenesis" version="2.6">
|
||||
<artifact name="objenesis-2.6.jar">
|
||||
<sha256 value="5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d" origin="Generated by Gradle"/>
|
||||
|
@ -3550,11 +3666,21 @@
|
|||
<sha256 value="fc0e57dec3836f2560b8d26dae8d2e330052b920b3028b49dfbd9c6b9a8ed0e2" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.slf4j" name="slf4j-api" version="1.6.4">
|
||||
<artifact name="slf4j-api-1.6.4.jar">
|
||||
<sha256 value="367b909030f714ee1176ab096b681e06348f03385e98d1bce0ed801b5452357e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.slf4j" name="slf4j-api" version="1.7.24">
|
||||
<artifact name="slf4j-api-1.7.24.jar">
|
||||
<sha256 value="baf3c7fe15fefeaf9e5b000d94547379dc48370f22a8797e239c127e7d7756ec" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.slf4j" name="slf4j-api" version="1.7.32">
|
||||
<artifact name="slf4j-api-1.7.32.jar">
|
||||
<sha256 value="3624f8474c1af46d75f98bc097d7864a323c81b3808aa43689a6e1c601c027be" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.smali" name="dexlib2" version="2.2.4">
|
||||
<artifact name="dexlib2-2.2.4.jar">
|
||||
<sha256 value="cb2677bfb66cfbc954e96e806ac6bda13051ad37754f9d1bcce38514e50e41e6" origin="Generated by Gradle"/>
|
||||
|
|
|
@ -14,6 +14,8 @@ include ':image-editor'
|
|||
include ':image-editor-app'
|
||||
include ':donations'
|
||||
include ':donations-app'
|
||||
include ':spinner'
|
||||
include ':spinner-app'
|
||||
|
||||
project(':app').name = 'Signal-Android'
|
||||
project(':paging').projectDir = file('paging/lib')
|
||||
|
@ -30,6 +32,9 @@ project(':image-editor-app').projectDir = file('image-editor/app')
|
|||
project(':donations').projectDir = file('donations/lib')
|
||||
project(':donations-app').projectDir = file('donations/app')
|
||||
|
||||
project(':spinner').projectDir = file('spinner/lib')
|
||||
project(':spinner-app').projectDir = file('spinner/app')
|
||||
|
||||
rootProject.name='Signal'
|
||||
|
||||
apply from: 'dependencies.gradle'
|
||||
|
|
21
spinner/README.md
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
|
||||
)
|
||||
}
|