diff --git a/app/build.gradle b/app/build.gradle
index 2196d42819..ec3db808cc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -80,113 +80,6 @@ protobuf {
}
}
-dependencies {
- implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
- implementation 'androidx.recyclerview:recyclerview:1.0.0'
- implementation 'com.google.android.material:material:1.0.0'
- implementation 'androidx.legacy:legacy-support-v13:1.0.0'
- implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'androidx.preference:preference:1.0.0'
- implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
- implementation 'androidx.gridlayout:gridlayout:1.0.0'
- implementation 'androidx.exifinterface:exifinterface:1.0.0'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation 'androidx.multidex:multidex:2.0.1'
- implementation 'androidx.navigation:navigation-fragment:2.1.0'
- implementation 'androidx.navigation:navigation-ui:2.1.0'
- implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
- implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
- implementation "androidx.camera:camera-core:1.0.0-alpha06"
- implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
-
- implementation('com.google.firebase:firebase-messaging:17.3.4') {
- exclude group: 'com.google.firebase', module: 'firebase-core'
- exclude group: 'com.google.firebase', module: 'firebase-analytics'
- exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
- }
-
- implementation 'com.google.android.gms:play-services-maps:16.1.0'
- implementation 'com.google.android.gms:play-services-auth:16.0.1'
-
- implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
- implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
-
- implementation 'org.conscrypt:conscrypt-android:2.0.0'
- implementation 'org.signal:aesgcmprovider:0.0.3'
-
- implementation project(':libsignal-service')
-
- implementation 'org.signal:argon2:13.1@aar'
-
- implementation 'org.signal:ringrtc-android:1.0.2'
-
- implementation "me.leolin:ShortcutBadger:1.1.16"
- implementation 'se.emilsjolander:stickylistheaders:2.7.0'
- implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
- implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
- implementation 'com.github.chrisbanes:PhotoView:2.1.3'
- implementation 'com.github.bumptech.glide:glide:4.9.0'
- annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
- annotationProcessor 'androidx.annotation:annotation:1.1.0'
- implementation 'com.makeramen:roundedimageview:2.1.0'
- implementation 'com.pnikosis:materialish-progress:1.5'
- implementation 'org.greenrobot:eventbus:3.0.0'
- implementation 'pl.tajchert:waitingdots:0.1.0'
- implementation 'com.melnykov:floatingactionbutton:1.3.0'
- implementation 'com.google.zxing:android-integration:3.1.0'
- implementation 'mobi.upod:time-duration-picker:1.1.3'
- implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
- implementation 'com.google.zxing:core:3.2.1'
- implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
- exclude group: 'com.android.support', module: 'support-annotations'
- }
- implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
- exclude group: 'com.android.support', module: 'appcompat-v7'
- }
- implementation ('com.tomergoldst.android:tooltips:1.0.6') {
- exclude group: 'com.android.support', module: 'appcompat-v7'
- }
- implementation ('com.klinkerapps:android-smsmms:4.0.1') {
- exclude group: 'com.squareup.okhttp', module: 'okhttp'
- exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
- }
- implementation 'com.annimon:stream:1.1.8'
- implementation ('com.takisoft.fix:colorpicker:0.9.1') {
- exclude group: 'com.android.support', module: 'appcompat-v7'
- exclude group: 'com.android.support', module: 'recyclerview-v7'
- }
-
- implementation 'com.airbnb.android:lottie:3.0.7'
-
- implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
- implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
- implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
- implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
- exclude group: 'com.fasterxml.jackson.core'
- exclude group: 'org.freemarker'
- }
-
- testImplementation 'junit:junit:4.12'
- testImplementation 'org.assertj:assertj-core:3.11.1'
- testImplementation 'org.mockito:mockito-core:1.9.5'
- testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
- testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
- testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
- testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
-
- testImplementation 'androidx.test:core:1.2.0'
- testImplementation 'org.robolectric:robolectric:4.2'
- testImplementation 'org.robolectric:shadows-multidex:4.2'
-
- androidTestImplementation 'androidx.test.ext:junit:1.1.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
-}
-
-dependencyVerification {
- configuration = '(play|website)(Debug|Release)RuntimeClasspath'
-}
-
def canonicalVersionCode = 610
def canonicalVersionName = "4.56.4"
@@ -307,6 +200,10 @@ android {
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
}
+ flipper {
+ initWith debug
+ minifyEnabled false
+ }
release {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
@@ -354,6 +251,117 @@ android {
}
}
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
+ implementation 'androidx.recyclerview:recyclerview:1.0.0'
+ implementation 'com.google.android.material:material:1.0.0'
+ implementation 'androidx.legacy:legacy-support-v13:1.0.0'
+ implementation 'androidx.cardview:cardview:1.0.0'
+ implementation 'androidx.preference:preference:1.0.0'
+ implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
+ implementation 'androidx.gridlayout:gridlayout:1.0.0'
+ implementation 'androidx.exifinterface:exifinterface:1.0.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.multidex:multidex:2.0.1'
+ implementation 'androidx.navigation:navigation-fragment:2.1.0'
+ implementation 'androidx.navigation:navigation-ui:2.1.0'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
+ implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
+ implementation "androidx.camera:camera-core:1.0.0-alpha06"
+ implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
+
+ implementation('com.google.firebase:firebase-messaging:17.3.4') {
+ exclude group: 'com.google.firebase', module: 'firebase-core'
+ exclude group: 'com.google.firebase', module: 'firebase-analytics'
+ exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
+ }
+
+ implementation 'com.google.android.gms:play-services-maps:16.1.0'
+ implementation 'com.google.android.gms:play-services-auth:16.0.1'
+
+ implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
+ implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
+
+ implementation 'org.conscrypt:conscrypt-android:2.0.0'
+ implementation 'org.signal:aesgcmprovider:0.0.3'
+
+ implementation project(':libsignal-service')
+
+ implementation 'org.signal:argon2:13.1@aar'
+
+ implementation 'org.signal:ringrtc-android:1.0.2'
+
+ implementation "me.leolin:ShortcutBadger:1.1.16"
+ implementation 'se.emilsjolander:stickylistheaders:2.7.0'
+ implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
+ implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
+ implementation 'com.github.chrisbanes:PhotoView:2.1.3'
+ implementation 'com.github.bumptech.glide:glide:4.9.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
+ annotationProcessor 'androidx.annotation:annotation:1.1.0'
+ implementation 'com.makeramen:roundedimageview:2.1.0'
+ implementation 'com.pnikosis:materialish-progress:1.5'
+ implementation 'org.greenrobot:eventbus:3.0.0'
+ implementation 'pl.tajchert:waitingdots:0.1.0'
+ implementation 'com.melnykov:floatingactionbutton:1.3.0'
+ implementation 'com.google.zxing:android-integration:3.1.0'
+ implementation 'mobi.upod:time-duration-picker:1.1.3'
+ implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
+ implementation 'com.google.zxing:core:3.2.1'
+ implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ }
+ implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
+ exclude group: 'com.android.support', module: 'appcompat-v7'
+ }
+ implementation ('com.tomergoldst.android:tooltips:1.0.6') {
+ exclude group: 'com.android.support', module: 'appcompat-v7'
+ }
+ implementation ('com.klinkerapps:android-smsmms:4.0.1') {
+ exclude group: 'com.squareup.okhttp', module: 'okhttp'
+ exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
+ }
+ implementation 'com.annimon:stream:1.1.8'
+ implementation ('com.takisoft.fix:colorpicker:0.9.1') {
+ exclude group: 'com.android.support', module: 'appcompat-v7'
+ exclude group: 'com.android.support', module: 'recyclerview-v7'
+ }
+
+ implementation 'com.airbnb.android:lottie:3.0.7'
+
+ implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
+ implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
+ implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
+ implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
+ exclude group: 'com.fasterxml.jackson.core'
+ exclude group: 'org.freemarker'
+ }
+
+ flipperImplementation 'com.facebook.flipper:flipper:0.32.2'
+ flipperImplementation 'com.facebook.soloader:soloader:0.8.2'
+
+ testImplementation 'junit:junit:4.12'
+ testImplementation 'org.assertj:assertj-core:3.11.1'
+ testImplementation 'org.mockito:mockito-core:1.9.5'
+ testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
+ testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
+ testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
+ testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
+
+ testImplementation 'androidx.test:core:1.2.0'
+ testImplementation 'org.robolectric:robolectric:4.2'
+ testImplementation 'org.robolectric:shadows-multidex:4.2'
+
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
+
+dependencyVerification {
+ configuration = '(play|website)(Debug|Release)RuntimeClasspath'
+}
+
+
def assembleWebsiteDescriptor = { variant, file ->
if (file.exists()) {
MessageDigest md = MessageDigest.getInstance("SHA-256");
diff --git a/app/src/flipper/AndroidManifest.xml b/app/src/flipper/AndroidManifest.xml
new file mode 100644
index 0000000000..1f15fab864
--- /dev/null
+++ b/app/src/flipper/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/app/src/flipper/java/org/thoughtcrime/securesms/FlipperApplicationContext.java b/app/src/flipper/java/org/thoughtcrime/securesms/FlipperApplicationContext.java
new file mode 100644
index 0000000000..7c5fe2c2db
--- /dev/null
+++ b/app/src/flipper/java/org/thoughtcrime/securesms/FlipperApplicationContext.java
@@ -0,0 +1,27 @@
+package org.thoughtcrime.securesms;
+
+import com.facebook.flipper.android.AndroidFlipperClient;
+import com.facebook.flipper.core.FlipperClient;
+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 org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
+
+public class FlipperApplicationContext extends ApplicationContext {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ SoLoader.init(this, false);
+
+ FlipperClient client = AndroidFlipperClient.getInstance(this);
+ client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
+ client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
+ client.addPlugin(new SharedPreferencesFlipperPlugin(this));
+ client.start();
+ }
+}
diff --git a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java
new file mode 100644
index 0000000000..786c616289
--- /dev/null
+++ b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java
@@ -0,0 +1,245 @@
+package org.thoughtcrime.securesms.database;
+
+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.sqlcipher.DatabaseUtils;
+import net.sqlcipher.database.SQLiteDatabase;
+import net.sqlcipher.database.SQLiteStatement;
+
+import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 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 {
+
+ public FlipperSqlCipherAdapter(Context context) {
+ super(context);
+ }
+
+ @Override
+ public List getDatabases() {
+ return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
+ }
+
+ @Override
+ public List getTableNames(Descriptor descriptor) {
+ SQLiteDatabase db = descriptor.getReadable();
+ List 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> 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 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 structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
+ List> 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 indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
+ List> indexesValues = new ArrayList<>();
+
+ try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) {
+ List 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> 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> cursorToList(Cursor cursor) {
+ List> rows = new ArrayList<>();
+ int numColumns = cursor.getColumnCount();
+
+ while (cursor.moveToNext()) {
+ List