diff --git a/app/build.gradle b/app/build.gradle index 6d89e2c8..f0f1a797 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,9 +45,14 @@ android { resourceConfigurations += ["en", "fr"] } -// androidResources { -// generateLocaleConfig true +// buildFeatures { +// compose true // } + +// composeOptions { +// kotlinCompilerExtensionVersion = "1.1.1" +// } + sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) @@ -128,6 +133,7 @@ dependencies { implementation project(':smswithoutborders_libsignal-doubleratchet') implementation 'androidx.room:room-testing:2.6.1' def paging_version = "3.2.1" + def m3 = "1.2.1" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' @@ -146,7 +152,6 @@ dependencies { implementation "androidx.work:work-multiprocess:2.9.0" implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'com.android.volley:volley:1.2.1' @@ -188,5 +193,8 @@ dependencies { implementation 'com.budiyev.android:code-scanner:2.1.0' + implementation 'com.google.android.material:material:1.11.0' + + implementation "androidx.compose.material3:material3:$m3" } diff --git a/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/13.json b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/13.json new file mode 100644 index 00000000..daa1e87a --- /dev/null +++ b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/13.json @@ -0,0 +1,591 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "1a131ad98d1f91a978b77d49221ec1c4", + "entities": [ + { + "tableName": "ThreadedConversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isSelf` INTEGER NOT NULL DEFAULT 0, `is_secured` INTEGER NOT NULL DEFAULT 0, `thread_id` TEXT NOT NULL, `address` TEXT, `msg_count` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `is_archived` INTEGER NOT NULL, `is_blocked` INTEGER NOT NULL, `is_shortcode` INTEGER NOT NULL, `is_read` INTEGER NOT NULL, `snippet` TEXT, `contact_name` TEXT, `formatted_datetime` TEXT, `is_mute` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`thread_id`))", + "fields": [ + { + "fieldPath": "isSelf", + "columnName": "isSelf", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "is_secured", + "columnName": "is_secured", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "msg_count", + "columnName": "msg_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "is_archived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_blocked", + "columnName": "is_blocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_shortcode", + "columnName": "is_shortcode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_read", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snippet", + "columnName": "snippet", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contact_name", + "columnName": "contact_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formatted_datetime", + "columnName": "formatted_datetime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "is_mute", + "columnName": "is_mute", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "thread_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomKeyStore", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `keystoreAlias` TEXT, `publicKey` TEXT, `privateKey` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keystoreAlias", + "columnName": "keystoreAlias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_CustomKeyStore_keystoreAlias", + "unique": true, + "columnNames": [ + "keystoreAlias" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_CustomKeyStore_keystoreAlias` ON `${TABLE_NAME}` (`keystoreAlias`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Archive", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`thread_id` TEXT NOT NULL, `is_archived` INTEGER NOT NULL, PRIMARY KEY(`thread_id`))", + "fields": [ + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "is_archived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "thread_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GatewayServer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`URL` TEXT, `protocol` TEXT, `tag` TEXT, `format` TEXT, `date` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "URL", + "columnName": "URL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GatewayClientProjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gatewayClientId` INTEGER NOT NULL, `name` TEXT, `binding1Name` TEXT, `binding2Name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gatewayClientId", + "columnName": "gatewayClientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "binding1Name", + "columnName": "binding1Name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "binding2Name", + "columnName": "binding2Name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationsThreadsEncryption", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `keystoreAlias` TEXT, `publicKey` TEXT, `states` TEXT, `exchangeDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keystoreAlias", + "columnName": "keystoreAlias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "states", + "columnName": "states", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exchangeDate", + "columnName": "exchangeDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ConversationsThreadsEncryption_keystoreAlias", + "unique": true, + "columnNames": [ + "keystoreAlias" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ConversationsThreadsEncryption_keystoreAlias` ON `${TABLE_NAME}` (`keystoreAlias`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Conversation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `message_id` TEXT, `thread_id` TEXT, `date` TEXT, `date_sent` TEXT, `type` INTEGER NOT NULL, `num_segments` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, `status` INTEGER NOT NULL, `error_code` INTEGER NOT NULL, `read` INTEGER NOT NULL, `is_encrypted` INTEGER NOT NULL, `is_key` INTEGER NOT NULL, `is_image` INTEGER NOT NULL, `formatted_date` TEXT, `address` TEXT, `text` TEXT, `data` TEXT, `_mk` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message_id", + "columnName": "message_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date_sent", + "columnName": "date_sent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "num_segments", + "columnName": "num_segments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscription_id", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error_code", + "columnName": "error_code", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_encrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_key", + "columnName": "is_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_image", + "columnName": "is_image", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "formatted_date", + "columnName": "formatted_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "_mk", + "columnName": "_mk", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Conversation_message_id", + "unique": true, + "columnNames": [ + "message_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Conversation_message_id` ON `${TABLE_NAME}` (`message_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "GatewayClient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `hostUrl` TEXT, `username` TEXT, `password` TEXT, `port` INTEGER NOT NULL, `friendlyConnectionName` TEXT, `virtualHost` TEXT, `connectionTimeout` INTEGER NOT NULL, `prefetch_count` INTEGER NOT NULL, `heartbeat` INTEGER NOT NULL, `protocol` TEXT, `projectName` TEXT, `projectBinding` TEXT, `projectBinding2` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hostUrl", + "columnName": "hostUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "friendlyConnectionName", + "columnName": "friendlyConnectionName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "virtualHost", + "columnName": "virtualHost", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connectionTimeout", + "columnName": "connectionTimeout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefetch_count", + "columnName": "prefetch_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "heartbeat", + "columnName": "heartbeat", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectName", + "columnName": "projectName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectBinding", + "columnName": "projectBinding", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectBinding2", + "columnName": "projectBinding2", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1a131ad98d1f91a978b77d49221ec1c4')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/DatabaseTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/DatabaseTest.java index a9ead086..a45d3562 100644 --- a/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/DatabaseTest.java +++ b/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/DatabaseTest.java @@ -24,8 +24,9 @@ public DatabaseTest() { } @Test public void testThreads() { + // [snippet, thread_id, msg_count] Cursor cursor = context.getContentResolver().query( - Telephony.Threads.CONTENT_URI, + Telephony.Sms.CONTENT_URI, null, null, null, diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java index eb4a3725..44c6b374 100644 --- a/app/src/androidTest/java/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java +++ b/app/src/androidTest/java/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java @@ -98,30 +98,109 @@ public void testIsValidAgreementPubKey() throws GeneralSecurityException, IOExce @Test public void testBuildForEncryptionRequest() throws Exception { String address = "+237333333333"; - byte[] transmissionRequest = E2EEHandler.buildForEncryptionRequest(context, address).second; + byte[] transmissionRequest = E2EEHandler + .buildForEncryptionRequest(context, address, null).second; assertTrue(E2EEHandler.isValidDefaultPublicKey(transmissionRequest)); } @Test public void canBeTransmittedAsData() throws Exception { String address = "+237444444444"; - byte[] transmissionRequest = E2EEHandler.buildForEncryptionRequest(context, address).second; + byte[] transmissionRequest = E2EEHandler + .buildForEncryptionRequest(context, address, null).second; assertTrue(transmissionRequest.length < 120); } + @Test + public void canSelfDoubleRatchet() throws Throwable { + String aliceAddress = "+2375555555550"; + String bobAddress = "+2375555555550"; + + // Initial request + Pair keystorePairAlice = E2EEHandler.buildForEncryptionRequest(context, + bobAddress, null); + String aliceKeystoreAlias = keystorePairAlice.first; + byte[] aliceTransmissionKey = keystorePairAlice.second; + + // Bob received Alice's key + assertTrue(E2EEHandler.isValidDefaultPublicKey(aliceTransmissionKey)); + byte[] aliceExtractedTransmissionKey = + E2EEHandler.extractTransmissionKey(aliceTransmissionKey); + String bobKeystoreAlias = E2EEHandler.deriveKeystoreAlias(aliceAddress, 0); + E2EEHandler.insertNewAgreementKeyDefault(context, aliceExtractedTransmissionKey, + bobKeystoreAlias); + + // assumption == bob initiates a reply to build key, but does not proceed to send + final String bobKeystoreAliasSelf = E2EEHandler.buildForSelf(bobKeystoreAlias); + Pair keystorePairBob = E2EEHandler.buildForEncryptionRequest(context, + aliceAddress, bobKeystoreAliasSelf); + byte[] bobTransmissionKey = keystorePairBob.second; + assertTrue(E2EEHandler.isValidDefaultPublicKey(bobTransmissionKey)); + byte[] bobExtractedTransmissionKey = + E2EEHandler.extractTransmissionKey(bobTransmissionKey); + E2EEHandler.insertNewAgreementKeyDefault(context, bobExtractedTransmissionKey, + bobKeystoreAliasSelf); + + // assumption == alice exist and self is true + assertTrue(E2EEHandler.isSelf(context, + E2EEHandler.deriveKeystoreAlias(aliceAddress, 0))); + + assertTrue(E2EEHandler.isAvailableInKeystore(aliceKeystoreAlias)); + assertTrue(E2EEHandler.isAvailableInKeystore(bobKeystoreAlias)); + + assertTrue(E2EEHandler.canCommunicateSecurely(context, aliceKeystoreAlias)); + assertTrue(E2EEHandler.canCommunicateSecurely(context, bobKeystoreAlias)); + + final boolean isSelf = E2EEHandler.isSelf(context, + E2EEHandler.deriveKeystoreAlias(aliceAddress, 0)); + assertTrue(isSelf); + // ----> alice sends the message + byte[] aliceText = CryptoHelpers.generateRandomBytes(130); + byte[][] aliceCipherText = E2EEHandler.encrypt(context, aliceKeystoreAlias, aliceText, true); + String aliceTransmissionText = E2EEHandler.buildTransmissionText(aliceCipherText[0]); + + // <----- bob receives the message + assertTrue(E2EEHandler.isValidDefaultText(aliceTransmissionText)); + byte[] aliceExtractedText = E2EEHandler.extractTransmissionText(aliceTransmissionText); + byte[] alicePlainText = E2EEHandler.decrypt(context, bobKeystoreAliasSelf, + aliceExtractedText, null, null, true); + assertArrayEquals(aliceText, alicePlainText); + + // <---- bob sends a message + byte[] bobText = CryptoHelpers.generateRandomBytes(130); + byte[][] bobCipherText = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText, true); + String bobTransmissionText = E2EEHandler.buildTransmissionText(bobCipherText[0]); + + // <---- then bob sends another + byte[] bobText1 = CryptoHelpers.generateRandomBytes(130); + byte[][] bobCipherText1 = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText1, true); + String bobTransmissionText1 = E2EEHandler.buildTransmissionText(bobCipherText1[0]); + + // <---- alice receives bob's message - this message is out of order + final String aliceKeystoreAliasSelf = E2EEHandler.buildForSelf(aliceKeystoreAlias); + assertTrue(E2EEHandler.isValidDefaultText(bobTransmissionText1)); + byte[] bobExtractedText_o3 = E2EEHandler.extractTransmissionText(bobTransmissionText1); + byte[] bobPlainText_o3 = E2EEHandler.decrypt(context, aliceKeystoreAliasSelf, + bobExtractedText_o3, null, null, true); + assertArrayEquals(bobText1, bobPlainText_o3); + + // <---- alice receives bob's message + assertTrue(E2EEHandler.isValidDefaultText(bobTransmissionText)); + byte[] bobExtractedText = E2EEHandler.extractTransmissionText(bobTransmissionText); + byte[] bobPlainText = E2EEHandler.decrypt(context, aliceKeystoreAliasSelf, bobExtractedText, + null, null, true); + assertArrayEquals(bobText, bobPlainText); + } + + @Test public void canDoubleRatchet() throws Throwable { - ConversationsThreadsEncryption conversationsThreadsEncryption = - new ConversationsThreadsEncryption(); - ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); String aliceAddress = "+237555555555"; String bobAddress = "+237666666666"; // Initial request String aliceKeystoreAlias = E2EEHandler.deriveKeystoreAlias(bobAddress, 0); Pair keystorePairAlice = E2EEHandler.buildForEncryptionRequest(context, - bobAddress); -// String aliceKeystoreAlias = keystorePairAlice.first; + bobAddress, null); byte[] aliceTransmissionKey = keystorePairAlice.second; // bob received alice's key @@ -131,8 +210,8 @@ public void canDoubleRatchet() throws Throwable { E2EEHandler.insertNewAgreementKeyDefault(context, aliceExtractedTransmissionKey, bobKeystoreAlias); // Agreement request - Pair keystorePairBob = E2EEHandler.buildForEncryptionRequest(context, aliceAddress); -// String bobKeystoreAlias = keystorePairBob.first; + Pair keystorePairBob = E2EEHandler + .buildForEncryptionRequest(context, aliceAddress, null); byte[] bobTransmissionKey = keystorePairBob.second; // alice received bob's key @@ -148,69 +227,69 @@ public void canDoubleRatchet() throws Throwable { // ----> alice sends the message byte[] aliceText = CryptoHelpers.generateRandomBytes(130); - byte[][] aliceCipherText = E2EEHandler.encrypt(context, aliceKeystoreAlias, aliceText); + byte[][] aliceCipherText = E2EEHandler.encrypt(context, aliceKeystoreAlias, aliceText, false); String aliceTransmissionText = E2EEHandler.buildTransmissionText(aliceCipherText[0]); // <----- bob receives the message assertTrue(E2EEHandler.isValidDefaultText(aliceTransmissionText)); byte[] aliceExtractedText = E2EEHandler.extractTransmissionText(aliceTransmissionText); byte[] alicePlainText = E2EEHandler.decrypt(context, bobKeystoreAlias, aliceExtractedText, - null, null); + null, null, false); assertArrayEquals(aliceText, alicePlainText); // <---- bob sends a message byte[] bobText = CryptoHelpers.generateRandomBytes(130); - byte[][] bobCipherText = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText); + byte[][] bobCipherText = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText, false); String bobTransmissionText = E2EEHandler.buildTransmissionText(bobCipherText[0]); // <----- bob receives the message again - as would be on mobile devices aliceExtractedText = E2EEHandler.extractTransmissionText(aliceTransmissionText); alicePlainText = E2EEHandler.decrypt(context, bobKeystoreAlias, aliceExtractedText, - aliceCipherText[1], null); + aliceCipherText[1], null, false); assertArrayEquals(aliceText, alicePlainText); // <---- alice receives bob's message assertTrue(E2EEHandler.isValidDefaultText(bobTransmissionText)); byte[] bobExtractedText = E2EEHandler.extractTransmissionText(bobTransmissionText); byte[] bobPlainText = E2EEHandler.decrypt(context, aliceKeystoreAlias, bobExtractedText, - null, null); + null, null, false); assertArrayEquals(bobText, bobPlainText); // <---- bob sends a message bobText = CryptoHelpers.generateRandomBytes(130); - bobCipherText = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText); + bobCipherText = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText, false); bobTransmissionText = E2EEHandler.buildTransmissionText(bobCipherText[0]); // <---- then bob sends another byte[] bobText1 = CryptoHelpers.generateRandomBytes(130); - byte[][] bobCipherText1 = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText1); + byte[][] bobCipherText1 = E2EEHandler.encrypt(context, bobKeystoreAlias, bobText1, false); String bobTransmissionText1 = E2EEHandler.buildTransmissionText(bobCipherText1[0]); // ----> alice sends the message aliceText = CryptoHelpers.generateRandomBytes(130); - aliceCipherText = E2EEHandler.encrypt(context, aliceKeystoreAlias, aliceText); + aliceCipherText = E2EEHandler.encrypt(context, aliceKeystoreAlias, aliceText, false); aliceTransmissionText = E2EEHandler.buildTransmissionText(aliceCipherText[0]); // <----- bob receives the message assertTrue(E2EEHandler.isValidDefaultText(aliceTransmissionText)); aliceExtractedText = E2EEHandler.extractTransmissionText(aliceTransmissionText); alicePlainText = E2EEHandler.decrypt(context, bobKeystoreAlias, aliceExtractedText, - null, null); + null, null, false); assertArrayEquals(aliceText, alicePlainText); // <---- alice receives bob's message - this message is out of order assertTrue(E2EEHandler.isValidDefaultText(bobTransmissionText1)); bobExtractedText = E2EEHandler.extractTransmissionText(bobTransmissionText1); bobPlainText = E2EEHandler.decrypt(context, aliceKeystoreAlias, bobExtractedText, - null, null); + null, null, false); assertArrayEquals(bobText1, bobPlainText); // <---- alice receives bob's message assertTrue(E2EEHandler.isValidDefaultText(bobTransmissionText)); bobExtractedText = E2EEHandler.extractTransmissionText(bobTransmissionText); bobPlainText = E2EEHandler.decrypt(context, aliceKeystoreAlias, bobExtractedText, - null, null); + null, null, false); assertArrayEquals(bobText, bobPlainText); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java index 90d0f2c5..2890996c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java @@ -2,7 +2,6 @@ import android.content.Context; -import android.database.Cursor; import android.provider.Telephony; import androidx.lifecycle.LiveData; @@ -14,7 +13,7 @@ import androidx.paging.PagingSource; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; -import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; @@ -80,19 +79,15 @@ public Conversation fetch(String messageId) throws InterruptedException { return datastore.conversationDao().getMessage(messageId); } - public long insert(Context context, Conversation conversation) throws InterruptedException { - long id = datastore.conversationDao().insert(conversation); - ThreadedConversations threadedConversations = - ThreadedConversations.build(context, conversation); - threadedConversations.setIs_read(true); - datastore.threadedConversationsDao().update(threadedConversations); + public long insert(Conversation conversation) throws InterruptedException { + datastore.threadedConversationsDao().insertThreadAndConversation(conversation); if(customPagingSource != null) customPagingSource.invalidate(); - return id; + return 0; } public void update(Conversation conversation) { - datastore.conversationDao().update(conversation); + datastore.conversationDao()._update(conversation); customPagingSource.invalidate(); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 30300903..78fbe980 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -1,9 +1,12 @@ package com.afkanerd.deku.DefaultSMS.AdaptersViewModels; +import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.provider.BlockedNumberContract; import android.provider.Telephony; +import android.text.TextUtils; +import android.util.Log; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -151,7 +154,7 @@ public void run() { } public void insert(ThreadedConversations threadedConversations) { - databaseConnector.threadedConversationsDao().insert(threadedConversations); + databaseConnector.threadedConversationsDao()._insert(threadedConversations); } public void reset(Context context) { @@ -166,7 +169,6 @@ public void reset(Context context) { } databaseConnector.conversationDao().insertAll(conversationList); - databaseConnector.threadedConversationsDao().deleteAll(); refresh(context); } @@ -182,23 +184,14 @@ public void delete(Context context, List ids) { } private void refresh(Context context) { - List newThreadedConversationsList = new ArrayList<>(); - Cursor cursor = context.getContentResolver().query( - Telephony.Threads.CONTENT_URI, - null, - null, - null, - "date DESC" - ); - - List threadedDraftsList = - databaseConnector.threadedConversationsDao().getThreadedDraftsList( - Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); - - List archivedThreads = databaseConnector.threadedConversationsDao().getArchivedList(); - List threadsIdsInDrafts = new ArrayList<>(); - for(ThreadedConversations threadedConversations : threadedDraftsList) - threadsIdsInDrafts.add(threadedConversations.getThread_id()); + try{ + Cursor cursor = context.getContentResolver().query( + Telephony.Threads.CONTENT_URI, + null, + null, + null, + "date DESC" + ); /* [date, rr, sub, subject, ct_t, read_status, reply_path_present, body, type, msg_box, @@ -206,62 +199,54 @@ private void refresh(Context context) { date_sent, read, ct_cls, m_size, rpt_a, address, sub_id, pri, tr_id, resp_txt, ct_l, m_cls, d_rpt, v, person, service_center, error_code, _id, m_type, status] */ - List threadedConversationsList = - databaseConnector.threadedConversationsDao().getAll(); - if(cursor != null && cursor.moveToFirst()) { - do { - ThreadedConversations threadedConversations = new ThreadedConversations(); - int recipientIdIndex = cursor.getColumnIndex("address"); - int snippetIndex = cursor.getColumnIndex("body"); - int dateIndex = cursor.getColumnIndex("date"); - int threadIdIndex = cursor.getColumnIndex("thread_id"); - int typeIndex = cursor.getColumnIndex("type"); - int readIndex = cursor.getColumnIndex("read"); - - threadedConversations.setIs_read(cursor.getInt(readIndex) == 1); - - threadedConversations.setAddress(cursor.getString(recipientIdIndex)); - if(threadedConversations.getAddress() == null || threadedConversations.getAddress().isEmpty()) - continue; - threadedConversations.setThread_id(cursor.getString(threadIdIndex)); - if(threadsIdsInDrafts.contains(threadedConversations.getThread_id())) { - ThreadedConversations tc = threadedDraftsList.get( - threadsIdsInDrafts.indexOf(threadedConversations.getThread_id())); - threadedConversations.setSnippet(tc.getSnippet()); - threadedConversations.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); - threadedConversations.setDate( - Long.parseLong(tc.getDate()) > - Long.parseLong(cursor.getString(dateIndex)) ? - tc.getDate() : cursor.getString(dateIndex)); - } - else { + List threadedConversationsList = new ArrayList<>(); + if(cursor != null && cursor.moveToFirst()) { + do { + ThreadedConversations threadedConversations = new ThreadedConversations(); + int recipientIdIndex = cursor.getColumnIndex("address"); + int snippetIndex = cursor.getColumnIndex("body"); + int dateIndex = cursor.getColumnIndex("date"); + int threadIdIndex = cursor.getColumnIndex("thread_id"); + int typeIndex = cursor.getColumnIndex("type"); + int readIndex = cursor.getColumnIndex("read"); + + threadedConversations.setIs_read(cursor.getInt(readIndex) == 1); + + threadedConversations.setAddress(cursor.getString(recipientIdIndex)); + if(threadedConversations.getAddress() == null || threadedConversations.getAddress().isEmpty()) + continue; + threadedConversations.setThread_id(cursor.getString(threadIdIndex)); threadedConversations.setSnippet(cursor.getString(snippetIndex)); threadedConversations.setType(cursor.getInt(typeIndex)); threadedConversations.setDate(cursor.getString(dateIndex)); - } - if(BlockedNumberContract.isBlocked(context, threadedConversations.getAddress())) - threadedConversations.setIs_blocked(true); - - threadedConversations.setIs_archived( - archivedThreads.contains(threadedConversations.getThread_id())); + if(BlockedNumberContract.isBlocked(context, threadedConversations.getAddress())) + threadedConversations.setIs_blocked(true); + + String contactName = Contacts.retrieveContactName(context, + threadedConversations.getAddress()); + threadedConversations.setContact_name(contactName); + threadedConversationsList.add(threadedConversations); + } while(cursor.moveToNext()); + cursor.close(); + } + databaseConnector.threadedConversationsDao().deleteAll(); + databaseConnector.threadedConversationsDao().insertAll(threadedConversationsList); + getCount(context); + } catch(Exception e) { + e.printStackTrace(); + loadNative(context); + } - String contactName = Contacts.retrieveContactName(context, - threadedConversations.getAddress()); - threadedConversations.setContact_name(contactName); + } - /** - * Check things that change first - * - Read status - * - Drafts - */ - if(!threadedConversationsList.contains(threadedConversations)) { - newThreadedConversationsList.add(threadedConversations); - } - } while(cursor.moveToNext()); - cursor.close(); + private void loadNative(Context context) { + try(Cursor cursor = NativeSMSDB.fetchAll(context)) { + List threadedConversations = + ThreadedConversations.buildRaw(cursor); + databaseConnector.threadedConversationsDao().insertAll(threadedConversations); + } catch (Exception e) { + e.printStackTrace(); } - databaseConnector.threadedConversationsDao().insertAll(newThreadedConversationsList); - getCount(context); } public void unarchive(List archiveList) { @@ -273,15 +258,15 @@ public void unblock(Context context, List threadIds) { databaseConnector.threadedConversationsDao().getList(threadIds); for(ThreadedConversations threadedConversations : threadedConversationsList) { BlockedNumberContract.unblock(context, threadedConversations.getAddress()); + threadedConversations.setIs_blocked(false); + databaseConnector.threadedConversationsDao().update(threadedConversations); } - refresh(context); } public void clearDrafts(Context context) { SMSDatabaseWrapper.deleteAllDraft(context); databaseConnector.threadedConversationsDao() .clearDrafts(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); - refresh(context); } public boolean hasUnread(List ids) { @@ -291,19 +276,16 @@ public boolean hasUnread(List ids) { public void markUnRead(Context context, List threadIds) { NativeSMSDB.Incoming.update_all_read(context, 0, threadIds.toArray(new String[0])); databaseConnector.threadedConversationsDao().updateRead(0, threadIds); - refresh(context); } public void markRead(Context context, List threadIds) { NativeSMSDB.Incoming.update_all_read(context, 1, threadIds.toArray(new String[0])); databaseConnector.threadedConversationsDao().updateRead(1, threadIds); - refresh(context); } public void markAllRead(Context context) { NativeSMSDB.Incoming.update_all_read(context, 1); databaseConnector.threadedConversationsDao().updateRead(1); - refresh(context); } public MutableLiveData> folderMetrics = new MutableLiveData<>(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java index 32030bcf..3e13b7e0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java @@ -6,13 +6,11 @@ import android.content.Intent; import android.provider.Telephony; import android.util.Base64; -import android.util.Log; import androidx.room.Room; -import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; -import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; @@ -20,19 +18,10 @@ import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.E2EE.E2EEHandler; -import com.google.i18n.phonenumbers.NumberParseException; //import org.bouncycastle.operator.OperatorCreationException; -import org.json.JSONException; - import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; public class IncomingDataSMSBroadcastReceiver extends BroadcastReceiver { @@ -46,10 +35,8 @@ public class IncomingDataSMSBroadcastReceiver extends BroadcastReceiver { public static String DATA_UPDATED_BROADCAST_INTENT = BuildConfig.APPLICATION_ID + ".DATA_UPDATED_BROADCAST_INTENT"; - - ExecutorService executorService = Executors.newFixedThreadPool(4); - Datastore databaseConnector; + @Override public void onReceive(Context context, Intent intent) { /** @@ -64,7 +51,6 @@ public void onReceive(Context context, Intent intent) { } databaseConnector = Datastore.datastore; - if (intent.getAction().equals(Telephony.Sms.Intents.DATA_SMS_RECEIVED_ACTION)) { if (getResultCode() == Activity.RESULT_OK) { try { @@ -95,24 +81,30 @@ public void onReceive(Context context, Intent intent) { ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { - databaseConnector.conversationDao().insert(conversation); + boolean isSelf = false; + boolean isSecured = false; if(isValidKey) { try { - processForEncryptionKey(context, conversation); - } catch (NumberParseException | IOException | InterruptedException | - GeneralSecurityException | JSONException e) { + boolean[] res = processForEncryptionKey(context, conversation); + isSelf = res[0]; + isSecured = res[1]; + } catch (Exception e) { e.printStackTrace(); } } + conversation.setIs_key(true); + conversation.setIs_encrypted(isSecured); + + ThreadedConversations threadedConversations = + databaseConnector.threadedConversationsDao() + .insertThreadAndConversation(conversation); Intent broadcastIntent = new Intent(DATA_DELIVER_ACTION); broadcastIntent.putExtra(Conversation.ID, messageId); broadcastIntent.putExtra(Conversation.THREAD_ID, threadId); context.sendBroadcast(broadcastIntent); - ThreadedConversations threadedConversations = - databaseConnector.threadedConversationsDao().get(threadId); if(!threadedConversations.isIs_mute()) NotificationsHandler.sendIncomingTextMessageNotification(context, conversation); @@ -126,11 +118,27 @@ public void run() { } } - void processForEncryptionKey(Context context, Conversation conversation) throws NumberParseException, GeneralSecurityException, IOException, InterruptedException, JSONException { + /** + * + * @param context + * @param conversation + * @return true if isSelf and false otherwise + * @throws Exception + */ + boolean[] processForEncryptionKey(Context context, Conversation conversation) throws + Exception { byte[] data = Base64.decode(conversation.getData(), Base64.DEFAULT); - String keystoreAlias = E2EEHandler.deriveKeystoreAlias(conversation.getAddress(), 0); + final String keystoreAlias = E2EEHandler.deriveKeystoreAlias(conversation.getAddress(), 0); byte[] extractedTransmissionKey = E2EEHandler.extractTransmissionKey(data); E2EEHandler.insertNewAgreementKeyDefault(context, extractedTransmissionKey, keystoreAlias); + final boolean isSelf = E2EEHandler.isSelf(context, keystoreAlias); +// Log.d(getClass().getName(), "Is self: " + isSelf); +// Log.d(getClass().getName(), "Is secured: " + E2EEHandler +// .canCommunicateSecurely(context, isSelf ? +// E2EEHandler.buildForSelf(keystoreAlias) : keystoreAlias, true)); + return new boolean[]{isSelf, + E2EEHandler.canCommunicateSecurely(context, isSelf ? + E2EEHandler.buildForSelf(keystoreAlias) : keystoreAlias, true)}; } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index e88b2c82..df24af96 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -11,18 +11,15 @@ import android.database.Cursor; import android.provider.Telephony; import android.util.Log; +import android.util.Pair; import androidx.room.Room; import com.afkanerd.deku.DefaultSMS.BuildConfig; -import com.afkanerd.deku.DefaultSMS.Commons.Helpers; -import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; -import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; -import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; @@ -31,8 +28,6 @@ import com.afkanerd.deku.Router.Router.RouterItem; import com.afkanerd.deku.Router.Router.RouterHandler; -import org.checkerframework.checker.units.qual.C; - import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -41,7 +36,6 @@ public class IncomingTextSMSBroadcastReceiver extends BroadcastReceiver { public static final String TAG_NAME = "RECEIVED_SMS_ROUTING"; public static final String TAG_ROUTING_URL = "swob.work.route.url,"; - public static String SMS_DELIVER_ACTION = BuildConfig.APPLICATION_ID + ".SMS_DELIVER_ACTION"; public static String SMS_SENT_BROADCAST_INTENT = @@ -79,7 +73,8 @@ public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION)) { if (getResultCode() == Activity.RESULT_OK) { try { - final String[] regIncomingOutput = NativeSMSDB.Incoming.register_incoming_text(context, intent); + final String[] regIncomingOutput = + NativeSMSDB.Incoming.register_incoming_text(context, intent); if(regIncomingOutput != null) { final String messageId = regIncomingOutput[NativeSMSDB.MESSAGE_ID]; final String body = regIncomingOutput[NativeSMSDB.BODY]; @@ -125,7 +120,7 @@ public void run() { e.printStackTrace(); } } - databaseConnector.conversationDao().update(conversation); + databaseConnector.conversationDao()._update(conversation); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -159,7 +154,7 @@ public void run() { conversation.setError_code(getResultCode()); } - databaseConnector.conversationDao().update(conversation); + databaseConnector.conversationDao()._update(conversation); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -187,7 +182,7 @@ public void run() { conversation.setError_code(getResultCode()); conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); } - databaseConnector.conversationDao().update(conversation); + databaseConnector.conversationDao()._update(conversation); Intent broadcastIntent = new Intent(DATA_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -214,7 +209,7 @@ public void run() { conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); } - databaseConnector.conversationDao().update(conversation); + databaseConnector.conversationDao()._update(conversation); Intent broadcastIntent = new Intent(DATA_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -226,18 +221,9 @@ public void run() { } } - public void insertThreads(Context context, Conversation conversation) { - ThreadedConversations threadedConversations = - ThreadedConversations.build(context, conversation); - String contactName = Contacts.retrieveContactName(context, conversation.getAddress()); - threadedConversations.setContact_name(contactName); - databaseConnector.threadedConversationsDao().insert(threadedConversations); - } - public void insertConversation(Context context, String address, String messageId, String threadId, String body, int subscriptionId, String date, String dateSent) { - Conversation conversation = new Conversation(); conversation.setMessage_id(messageId); conversation.setThread_id(threadId); @@ -251,43 +237,46 @@ public void insertConversation(Context context, String address, String messageId public void run() { String text = body; try { - text = processEncryptedIncoming(context, address, body); + Pair res = processEncryptedIncoming(context, address, body); + text = res.first; + conversation.setIs_encrypted(res.second); } catch (Throwable e) { e.printStackTrace(); } conversation.setText(text); try { - databaseConnector.conversationDao().insert(conversation); - insertThreads(context, conversation); + ThreadedConversations threadedConversations = + databaseConnector.threadedConversationsDao() + .insertThreadAndConversation(conversation); + if(!threadedConversations.isIs_mute()) + NotificationsHandler.sendIncomingTextMessageNotification(context, + conversation); + } catch (Exception e) { e.printStackTrace(); } - Intent broadcastIntent = new Intent(SMS_DELIVER_ACTION); broadcastIntent.putExtra(Conversation.ID, messageId); context.sendBroadcast(broadcastIntent); - -// String defaultRegion = Helpers.getUserCountry(context); -// String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); - ThreadedConversations threadedConversations = - databaseConnector.threadedConversationsDao().get(threadId); - if(!threadedConversations.isIs_mute()) - NotificationsHandler.sendIncomingTextMessageNotification(context, - conversation); - } }); } - public String processEncryptedIncoming(Context context, String address, String text) throws Throwable { + public Pair processEncryptedIncoming(Context context, String address, String text) throws Throwable { + boolean encrypted = false; if(E2EEHandler.isValidDefaultText(text)) { String keystoreAlias = E2EEHandler.deriveKeystoreAlias(address, 0); byte[] cipherText = E2EEHandler.extractTransmissionText(text); - text = new String(E2EEHandler.decrypt(context, keystoreAlias, cipherText, null, null)); + boolean isSelf = E2EEHandler.isSelf(context, keystoreAlias); + Log.d(getClass().getName(), "Decrypting incoming: " + text); + text = new String(E2EEHandler.decrypt(context, isSelf ? + E2EEHandler.buildForSelf(keystoreAlias) :keystoreAlias, + cipherText, null, null, isSelf)); + encrypted = true; } - return text; + return new Pair<>(text, encrypted); } public void router_activities(Context context, String messageId) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java index d2eaf42e..a34d5120 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java @@ -24,6 +24,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; @@ -87,7 +88,8 @@ public void onReceive(Context context, Intent intent) { @Override public void run() { try { - databaseConnector.conversationDao().insert(conversation); + databaseConnector.threadedConversationsDao() + .insertThreadAndConversation(conversation); SMSDatabaseWrapper.send_text(context, conversation, null); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); @@ -99,17 +101,22 @@ public void run() { context.sendBroadcast(broadcastIntent); NotificationCompat.MessagingStyle messagingStyle = - NotificationsHandler.getMessagingStyle(context, conversation, reply.toString()); + NotificationsHandler.getMessagingStyle(context, conversation, + reply.toString()); - Intent replyIntent = NotificationsHandler.getReplyIntent(context, conversation); - PendingIntent pendingIntent = NotificationsHandler.getPendingIntent(context, conversation); + Intent replyIntent = NotificationsHandler + .getReplyIntent(context, conversation); + + PendingIntent pendingIntent = NotificationsHandler + .getPendingIntent(context, conversation); NotificationCompat.Builder builder = NotificationsHandler.getNotificationBuilder(context, replyIntent, conversation, pendingIntent); builder.setStyle(messagingStyle); - NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context); + NotificationManagerCompat notificationManagerCompat = + NotificationManagerCompat.from(context); notificationManagerCompat.notify(Integer.parseInt(threadId), builder.build()); } catch (Exception e) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Commons/Helpers.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Commons/Helpers.java index 84ce1bf4..bc5442a4 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Commons/Helpers.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Commons/Helpers.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.res.Resources; import android.content.res.TypedArray; +import android.database.Cursor; import android.graphics.Color; import android.net.ConnectivityManager; import android.net.Uri; @@ -29,11 +30,14 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.R; +import com.google.android.material.navigation.NavigationBarItemView; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; +import java.lang.annotation.Native; import java.security.SecureRandom; import java.sql.Date; import java.text.SimpleDateFormat; @@ -133,6 +137,20 @@ public static boolean isShortCode(String address) { return matcher.find(); } + public static String getFormatCompleteNumber(Context context, String address, String defaultRegion) { + try(Cursor cursor = NativeSMSDB.fetchByAddress(context, address)) { + if(cursor.moveToFirst()) { + int recipientIdIndex = cursor.getColumnIndexOrThrow("address"); + address = cursor.getString(recipientIdIndex); + } + cursor.close(); + } catch(Exception e) { + e.printStackTrace(); + } + + return address; + } + public static String getFormatCompleteNumber(String data, String defaultRegion) { data = data.replaceAll("%2B", "+") .replaceAll("-", "") @@ -480,9 +498,13 @@ public void onClick(View widget) { widget.getContext().startActivity(intent); } }; - spannableString.setSpan(clickableSpan, start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - spannableString.setSpan(new ForegroundColorSpan(color), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + try { + spannableString.setSpan(clickableSpan, start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannableString.setSpan(new ForegroundColorSpan(color), start, end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } catch(Exception e) { + e.printStackTrace(); + } } else if (_string.matches( "\\(*\\+*[1-9]{0,3}\\)*-*[1-9]{0,3}[-. \\/]*\\(*[2-9]\\d{2}\\)*[-. \\/]*\\d{3}[-. \\/]*\\d{4} *e*x*t*\\.* *\\d{0,4}" + diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 04dd7553..8b8c0bfb 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -17,6 +17,7 @@ import android.telephony.SmsManager; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -100,6 +101,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_conversations); // test(); + toolbar = (Toolbar) findViewById(R.id.conversation_toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); @@ -117,6 +119,11 @@ protected void onCreate(Bundle savedInstanceState) { } } + @Override + protected void onStart() { + super.onStart(); + } + public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { // Always call the superclass so it can restore the view hierarchy. super.onRestoreInstanceState(savedInstanceState); @@ -138,20 +145,25 @@ protected void onResume() { super.onResume(); TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); layout.requestFocus(); - - if(threadedConversations.is_secured) - layout.setPlaceholderText(getString(R.string.send_message_secured_text_box_hint)); - ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { - NativeSMSDB.Incoming.update_read(getApplicationContext(), 1, - threadedConversations.getThread_id(), null); - conversationsViewModel.updateToRead(getApplicationContext()); + NativeSMSDB.Incoming.update_read(getApplicationContext(), + 1, threadedConversations.getThread_id(), + null); + conversationsViewModel + .updateToRead(getApplicationContext()); + +// ThreadedConversations threadedConversations1 = +// databaseConnector.threadedConversationsDao() +// .get(threadedConversations.getThread_id()); +// threadedConversations1.setIs_read(true); +// databaseConnector.threadedConversationsDao().update(threadedConversations1); + threadedConversations.setIs_read(true); databaseConnector.threadedConversationsDao().update(threadedConversations); - }catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -269,33 +281,28 @@ private void configureActivityDependencies() throws Exception { throw new Exception("No threadId nor Address supplied for activity"); } if(getIntent().hasExtra(Conversation.THREAD_ID)) { - ThreadedConversations threadedConversations = new ThreadedConversations(); - threadedConversations.setThread_id(getIntent().getStringExtra(Conversation.THREAD_ID)); - this.threadedConversations = ThreadedConversationsHandler.get( - databaseConnector.threadedConversationsDao(), threadedConversations); + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + threadedConversations = databaseConnector.threadedConversationsDao() + .get(getIntent().getStringExtra(Conversation.THREAD_ID)); + } + }); + thread.start(); + thread.join(); } else if(getIntent().hasExtra(Conversation.ADDRESS)) { - ThreadedConversations threadedConversations = new ThreadedConversations(); - threadedConversations.setAddress(getIntent().getStringExtra(Conversation.ADDRESS)); this.threadedConversations = ThreadedConversationsHandler.get(getApplicationContext(), getIntent().getStringExtra(Conversation.ADDRESS)); } - final String defaultUserCountry = Helpers.getUserCountry(getApplicationContext()); - final String address = this.threadedConversations.getAddress(); - this.threadedConversations.setAddress( - Helpers.getFormatCompleteNumber(address, defaultUserCountry)); String contactName = Contacts.retrieveContactName(getApplicationContext(), this.threadedConversations.getAddress()); - if(contactName == null) { - this.threadedConversations.setContact_name(Helpers.getFormatNationalNumber(address, - defaultUserCountry )); - } else { - this.threadedConversations.setContact_name(contactName); - } + this.threadedConversations.setContact_name(contactName); setEncryptionThreadedConversations(this.threadedConversations); isShortCode = Helpers.isShortCode(this.threadedConversations); + attachObservers(); } int searchPointerPosition; @@ -428,32 +435,6 @@ public void onChanged(PagingData conversationPagingData) { conversationPagingData); } }); - broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String messageId = intent.getStringExtra(Conversation.ID); - ThreadingPoolExecutor.executorService.execute(new Runnable() { - @Override - public void run() { - Conversation conversation = databaseConnector.conversationDao() - .getMessage(messageId); - conversation.setRead(true); - conversationsViewModel.update(conversation); - } - }); - } - }; - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_DELIVER_ACTION); - intentFilter.addAction(IncomingDataSMSBroadcastReceiver.DATA_DELIVER_ACTION); - - intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_UPDATED_BROADCAST_INTENT); - intentFilter.addAction(IncomingDataSMSBroadcastReceiver.DATA_UPDATED_BROADCAST_INTENT); - - if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) - registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED); - else - registerReceiver(broadcastReceiver, intentFilter); } else if(this.threadedConversations.getThread_id()!= null && !this.threadedConversations.getThread_id().isEmpty()) { @@ -546,11 +527,12 @@ private void configureToolbars() { } private String getAbTitle() { - String abTitle = getIntent().getStringExtra(Conversation.ADDRESS); - if(this.threadedConversations == null || this.threadedConversations.getContact_name() == null) { - this.threadedConversations.setContact_name( - Contacts.retrieveContactName(getApplicationContext(), abTitle)); - } +// String abTitle = getIntent().getStringExtra(Conversation.ADDRESS); +// String abTitle = threadedConversations.getAddress(); +// if(this.threadedConversations == null || this.threadedConversations.getContact_name() == null) { +// this.threadedConversations.setContact_name( +// Contacts.retrieveContactName(getApplicationContext(), abTitle)); +// } return (this.threadedConversations.getContact_name() != null && !this.threadedConversations.getContact_name().isEmpty()) ? this.threadedConversations.getContact_name(): this.threadedConversations.getAddress(); @@ -569,7 +551,8 @@ private String getAbSubTitle() { @Override protected void onPause() { super.onPause(); - if (smsTextView.getText() != null && !smsTextView.getText().toString().isEmpty()) { + if (smsTextView != null && smsTextView.getText() != null && + !smsTextView.getText().toString().isEmpty()) { try { saveDraft(String.valueOf(System.currentTimeMillis()), smsTextView.getText().toString(), threadedConversations); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java index b7f1ade8..7bc64695 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java @@ -26,11 +26,14 @@ import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; +import com.afkanerd.deku.E2EE.ConversationsThreadsEncryption; +import com.afkanerd.deku.E2EE.ConversationsThreadsEncryptionDao; import com.afkanerd.deku.E2EE.E2EEHandler; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.google.i18n.phonenumbers.NumberParseException; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; @@ -38,8 +41,6 @@ import java.util.concurrent.Executors; public class CustomAppCompactActivity extends DualSIMConversationActivity { - protected final static String DRAFT_PRESENT_BROADCAST = "DRAFT_PRESENT_BROADCAST"; - protected ConversationsViewModel conversationsViewModel; protected ThreadedConversationsViewModel threadedConversationsViewModel; @@ -88,16 +89,40 @@ protected void sendTextMessage(Conversation conversation, threadedConversations, messageId, null); } - protected void sendTextMessage(final String text, int subscriptionId, + protected void sendTextMessage(String text, int subscriptionId, ThreadedConversations threadedConversations, String messageId, byte[] _mk) throws NumberParseException, InterruptedException { if(text != null) { if(messageId == null) messageId = String.valueOf(System.currentTimeMillis()); - final String messageIdFinal = messageId; + Conversation conversation = new Conversation(); + if(_mk != null) { + try { + String keystoreAlias = E2EEHandler.deriveKeystoreAlias( + threadedConversations.getAddress(), 0); + if(threadedConversations.isSelf()) + keystoreAlias = E2EEHandler.buildForSelf(keystoreAlias); + byte[] cipherText = E2EEHandler.extractTransmissionText(text); + ConversationsThreadsEncryption conversationsThreadsEncryption = + databaseConnector.conversationsThreadsEncryptionDao() + .fetch(keystoreAlias); + byte[] AD = Base64.decode(conversationsThreadsEncryption.getPublicKey(), Base64.NO_WRAP); + String plainText = new String(E2EEHandler.decrypt(getApplicationContext(), + keystoreAlias, cipherText, _mk, AD, threadedConversations.isSelf()), + StandardCharsets.UTF_8); + conversation.setText(plainText); + conversation.setIs_encrypted(true); + } catch(Throwable e ) { + e.printStackTrace(); + conversation.setText(text); + } + } else { + conversation.setText(text); + } + + final String messageIdFinal = messageId; conversation.setMessage_id(messageId); - conversation.setText(text); conversation.setThread_id(threadedConversations.getThread_id()); conversation.setSubscription_id(subscriptionId); conversation.setType(Telephony.Sms.MESSAGE_TYPE_OUTBOX); @@ -105,21 +130,30 @@ protected void sendTextMessage(final String text, int subscriptionId, conversation.setAddress(threadedConversations.getAddress()); conversation.setStatus(Telephony.Sms.STATUS_PENDING); // TODO: should encrypt this before storing - if(_mk != null) - conversation.set_mk(Base64.encodeToString(_mk, Base64.NO_WRAP)); +// if(_mk != null) +// conversation.set_mk(Base64.encodeToString(_mk, Base64.NO_WRAP)); if(conversationsViewModel != null) { ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { - conversationsViewModel.insert(getApplicationContext(), conversation); - SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, null); -// conversationsViewModel.updateThreadId(conversation.getThread_id(), -// _messageId, id); + conversationsViewModel.insert(conversation); + } catch(Exception e) { + e.printStackTrace(); + return; + } + + try { + if(_mk == null) + SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, null); + else + SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, + text, null); } catch (Exception e) { e.printStackTrace(); - NativeSMSDB.Outgoing.register_failed(getApplicationContext(), messageIdFinal, 1); + NativeSMSDB.Outgoing.register_failed(getApplicationContext(), + messageIdFinal, 1); conversation.setStatus(Telephony.TextBasedSmsColumns.STATUS_FAILED); conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); conversation.setError_code(1); @@ -146,20 +180,14 @@ public void run() { conversation.setDate(String.valueOf(System.currentTimeMillis())); conversation.setAddress(threadedConversations.getAddress()); conversation.setStatus(Telephony.Sms.STATUS_PENDING); + conversation.setIs_encrypted(threadedConversations.isIs_secured()); + Log.d(getClass().getName(), "Saving draft"); try { - conversationsViewModel.insert(getApplicationContext(), conversation); - - ThreadedConversations tc = - ThreadedConversations.build(getApplicationContext(), conversation); - databaseConnector.threadedConversationsDao().insert(tc); - + conversationsViewModel.insert(conversation); SMSDatabaseWrapper.saveDraft(getApplicationContext(), conversation); } catch (Exception e) { e.printStackTrace(); } - - Intent intent = new Intent(DRAFT_PRESENT_BROADCAST); - sendBroadcast(intent); } }); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java index 5a40d0e0..b948ca90 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java @@ -1,21 +1,14 @@ package com.afkanerd.deku.DefaultSMS.DAO; -import android.provider.Telephony; - -import androidx.lifecycle.LiveData; -import androidx.paging.DataSource; -import androidx.paging.PagingLiveData; import androidx.paging.PagingSource; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; -import androidx.room.Transaction; import androidx.room.Update; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; -import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import java.util.List; @@ -39,6 +32,10 @@ public interface ConversationDao { "latest_items WHERE thread_id IS NOT NULL ORDER BY date DESC") List getForThreading(); + + @Query("SELECT * FROM Conversation WHERE thread_id =:threadId ORDER BY date DESC LIMIT 1") + Conversation fetchLatestForThread(String threadId); + @Query("SELECT * FROM Conversation ORDER BY date DESC") List getComplete(); @@ -52,14 +49,14 @@ public interface ConversationDao { @Query("SELECT * FROM Conversation WHERE message_id =:message_id") Conversation getMessage(String message_id); - @Insert(onConflict = OnConflictStrategy.REPLACE) - long insert(Conversation conversation); + @Insert + long _insert(Conversation conversation); @Insert(onConflict = OnConflictStrategy.REPLACE) List insertAll(List conversationList); @Update - int update(Conversation conversation); + int _update(Conversation conversation); @Update int update(List conversations); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java index 89abf8ca..6d449ca2 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java @@ -1,5 +1,8 @@ package com.afkanerd.deku.DefaultSMS.DAO; +import android.provider.Telephony; +import android.util.Log; + import androidx.lifecycle.LiveData; import androidx.paging.PagingSource; import androidx.room.Dao; @@ -13,6 +16,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Archive; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import java.util.List; @@ -75,7 +79,8 @@ public interface ThreadedConversationsDao { "ThreadedConversations.msg_count, ThreadedConversations.is_archived, " + "ThreadedConversations.is_blocked, ThreadedConversations.is_read, " + "ThreadedConversations.is_shortcode, ThreadedConversations.contact_name, " + - "ThreadedConversations.is_mute, ThreadedConversations.is_secured " + + "ThreadedConversations.is_mute, ThreadedConversations.is_secured, " + + "ThreadedConversations.isSelf " + "FROM Conversation, ThreadedConversations WHERE " + "Conversation.type = :type AND ThreadedConversations.thread_id = Conversation.thread_id " + "ORDER BY Conversation.date DESC") @@ -87,7 +92,8 @@ public interface ThreadedConversationsDao { "Conversation.date, Conversation.type, Conversation.read, " + "0 as msg_count, ThreadedConversations.is_archived, ThreadedConversations.is_blocked, " + "ThreadedConversations.is_read, ThreadedConversations.is_shortcode, " + - "ThreadedConversations.is_mute, ThreadedConversations.is_secured " + + "ThreadedConversations.is_mute, ThreadedConversations.is_secured, " + + "ThreadedConversations.isSelf " + "FROM Conversation, ThreadedConversations WHERE " + "Conversation.type = :type AND ThreadedConversations.thread_id = Conversation.thread_id " + "ORDER BY Conversation.date DESC") @@ -187,12 +193,61 @@ default void updateRead(int read, long id) { "LIKE '%' || :search_string || '%' GROUP BY thread_id ORDER BY date DESC") List findByThread(String search_string, String thread_id); - @Insert(onConflict = OnConflictStrategy.REPLACE) - long insert(ThreadedConversations threadedConversations); + @Insert + long _insert(ThreadedConversations threadedConversations); + @Transaction + default ThreadedConversations insertThreadAndConversation(Conversation conversation) { + /* - Import things are: + 1. Dates + 2. Snippet + 3. ThreadId + */ + final String dates = conversation.getDate(); + final String snippet = conversation.getText(); + final String threadId = conversation.getThread_id(); + final String address = conversation.getAddress(); + + final int type = conversation.getType(); + + final boolean isRead = type != Telephony.Sms.MESSAGE_TYPE_INBOX || conversation.isRead(); + final boolean isSecured = conversation.isIs_encrypted(); + + boolean insert = false; + ThreadedConversations threadedConversations = Datastore.datastore.threadedConversationsDao() + .get(conversation.getThread_id()); + if(threadedConversations == null) { + threadedConversations = new ThreadedConversations(); + threadedConversations.setThread_id(threadId); + insert = true; + } + threadedConversations.setDate(dates); + threadedConversations.setSnippet(snippet); + threadedConversations.setIs_read(isRead); + threadedConversations.setIs_secured(isSecured); + threadedConversations.setAddress(address); + threadedConversations.setType(type); + + long id = Datastore.datastore.conversationDao()._insert(conversation); + if(insert) + Datastore.datastore.threadedConversationsDao()._insert(threadedConversations); + else { + Datastore.datastore.threadedConversationsDao().update(threadedConversations); + } + + return threadedConversations; + } @Update - int update(ThreadedConversations threadedConversations); + int _update(ThreadedConversations threadedConversations); + + @Transaction + default long update(ThreadedConversations threadedConversations) { + if(threadedConversations.getDate() == null || threadedConversations.getDate().isEmpty()) + threadedConversations.setDate(Datastore.datastore.conversationDao() + .fetchLatestForThread(threadedConversations.getThread_id()).getDate()); + return _update(threadedConversations); + } @Delete void delete(ThreadedConversations threadedConversations); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ModalSheetFragment.kt b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ModalSheetFragment.kt new file mode 100644 index 00000000..6277b084 --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ModalSheetFragment.kt @@ -0,0 +1,76 @@ +package com.afkanerd.deku.DefaultSMS.Fragments + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Pair +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnClickListener +import android.view.ViewGroup +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor +import com.afkanerd.deku.DefaultSMS.R +import com.afkanerd.deku.E2EE.E2EEHandler +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import java.util.concurrent.ThreadPoolExecutor + +class ModalSheetFragment(var threadedConversations: ThreadedConversations) : BottomSheetDialogFragment() { + + lateinit var bottomSheetBehavior: BottomSheetBehavior + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_modalsheet, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val bottomSheet = view.findViewById(R.id.conversations_bottom_sheet_view_id) + + // Get the BottomSheetBehavior instance + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + bottomSheetBehavior.isFitToContents = true + bottomSheetBehavior.isDraggable = true + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + view.findViewById(R.id.conversation_secure_request_agree_read_more_btn) + .setOnClickListener(OnClickListener { clickPrivacyPolicy(it) }) + } + + companion object { + const val TAG = "ModalBottomSheet" + } + + private fun clickPrivacyPolicy(view: View?) { + val url = getString(R.string.conversations_secure_conversation_request_information_deku_encryption_link) + val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + view?.context?.startActivity(shareIntent) + } + + private fun agreeToSecure() { + var keystoreAlias = E2EEHandler.deriveKeystoreAlias(threadedConversations.address, 0) + ThreadingPoolExecutor.executorService.execute { + if (threadedConversations.isSelf) { + keystoreAlias = E2EEHandler.buildForSelf(keystoreAlias) + } + val keystorePair: Pair = + E2EEHandler.buildForEncryptionRequest(context, + threadedConversations.address, keystoreAlias) + val transmissionKey: ByteArray = E2EEHandler.extractTransmissionKey(keystorePair.second) + E2EEHandler.insertNewAgreementKeyDefault(context, transmissionKey, keystoreAlias) + val tc: ThreadedConversations = + Datastore.datastore.threadedConversationsDao() + .get(threadedConversations.thread_id) + tc.isIs_secured = true + Datastore.datastore.threadedConversationsDao().update(tc); + threadedConversations = tc + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java index d71daf87..9d0b4054 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java @@ -2,31 +2,18 @@ import android.content.Context; import android.provider.Telephony; +import android.util.Log; import androidx.room.Room; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; +import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import java.util.List; public class ConversationHandler { - public static ConversationDao conversationDao; - - public static Conversation buildConversationForSending(Context context, String body, int subscriptionId, - String address) { - long threadId = Telephony.Threads.getOrCreateThreadId(context, address); - Conversation conversation = new Conversation(); - conversation.setMessage_id(String.valueOf(System.currentTimeMillis())); - conversation.setText(body); - conversation.setSubscription_id(subscriptionId); - conversation.setType(Telephony.Sms.MESSAGE_TYPE_OUTBOX); - conversation.setDate(String.valueOf(System.currentTimeMillis())); - conversation.setAddress(address); - conversation.setThread_id(String.valueOf(threadId)); - conversation.setStatus(Telephony.Sms.STATUS_PENDING); - return conversation; - } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java index 2415364f..7b6d04d4 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java @@ -31,6 +31,17 @@ public void setIs_secured(boolean is_secured) { this.is_secured = is_secured; } + public boolean isSelf() { + return isSelf; + } + + public void setSelf(boolean self) { + isSelf = self; + } + + @ColumnInfo(defaultValue = "0") + private boolean isSelf = false; + @ColumnInfo(defaultValue = "0") public boolean is_secured = false; @NonNull @@ -83,6 +94,7 @@ public static ThreadedConversations build(Context context, Conversation conversa threadedConversations.setSnippet(context.getString(R.string.conversation_threads_secured_content)); } else threadedConversations.setSnippet(conversation.getText()); + threadedConversations.setIs_secured(conversation.isIs_encrypted()); threadedConversations.setThread_id(conversation.getThread_id()); threadedConversations.setDate(conversation.getDate()); threadedConversations.setType(conversation.getType()); @@ -298,6 +310,8 @@ public boolean equals(@Nullable Object obj) { threadedConversations.type == this.type && threadedConversations.msg_count == this.msg_count && threadedConversations.is_mute == this.is_mute && + threadedConversations.is_secured == this.is_secured && + threadedConversations.isSelf == this.isSelf && Objects.equals(threadedConversations.date, this.date) && Objects.equals(threadedConversations.address, this.address) && Objects.equals(threadedConversations.contact_name, this.contact_name) && diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java index 7aae872f..c257ca0a 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java @@ -2,35 +2,47 @@ import android.content.Context; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.provider.Telephony; +import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; +import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; public class ThreadedConversationsHandler { public static ThreadedConversations get(Context context, String address) { + final String defaultUserCountry = Helpers.getUserCountry(context); long threadId = Telephony.Threads.getOrCreateThreadId(context, address); ThreadedConversations threadedConversations = new ThreadedConversations(); - threadedConversations.setAddress(address); + threadedConversations.setAddress(Helpers.getFormatCompleteNumber(address, defaultUserCountry)); threadedConversations.setThread_id(String.valueOf(threadId)); return threadedConversations; } - public static ThreadedConversations get(ThreadedConversationsDao threadedConversationsDao, + public static ThreadedConversations get(Context context, ThreadedConversations threadedConversations) throws InterruptedException { - final ThreadedConversations[] threadedConversations1 = {threadedConversations}; - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - threadedConversations1[0] = threadedConversationsDao - .get(threadedConversations.getThread_id()); - } - }); - thread.start(); - thread.join(); - - return threadedConversations1[0]; +// final ThreadedConversations[] threadedConversations1 = {threadedConversations}; +// Thread thread = new Thread(new Runnable() { +// @Override +// public void run() { +// threadedConversations1[0] = threadedConversationsDao +// .get(threadedConversations.getThread_id()); +// } +// }); +// thread.start(); +// thread.join(); + try(Cursor cursor = + NativeSMSDB.fetchByThreadId(context, threadedConversations.getThread_id())) { + if(cursor.moveToFirst()) + threadedConversations = ThreadedConversations.build(cursor); + } catch (Exception e) { + e.printStackTrace(); + } + return threadedConversations; + +// return threadedConversations1[0]; } public static void call(Context context, ThreadedConversations threadedConversations) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java index 06665847..9a59a34e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java @@ -12,6 +12,7 @@ import android.graphics.drawable.Drawable; import android.provider.Telephony; import android.util.Log; +import android.util.Pair; import android.view.View; import android.widget.ImageView; import android.widget.TextView; @@ -24,6 +25,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.R; +import com.afkanerd.deku.E2EE.E2EEHandler; import com.google.android.material.card.MaterialCardView; import java.util.List; @@ -65,46 +67,50 @@ public ThreadedConversationsTemplateViewHolder(@NonNull View itemView) { public void bind(ThreadedConversations conversation, View.OnClickListener onClickListener, View.OnLongClickListener onLongClickListener, String defaultRegion) { - this.id = String.valueOf(conversation.getThread_id()); - - int contactColor = Helpers.getColor(itemView.getContext(), id); - if(conversation.getContact_name() != null && !conversation.getContact_name().isEmpty()) { - this.contactAvatar.setVisibility(View.GONE); - this.contactInitials.setVisibility(View.VISIBLE); - this.contactInitials.setAvatarInitials(conversation.getContact_name().contains(" ") ? - conversation.getContact_name() : conversation.getContact_name().substring(0, 1)); - this.contactInitials.setAvatarInitialsBackgroundColor(contactColor); - } - else { - this.contactAvatar.setVisibility(View.VISIBLE); - this.contactInitials.setVisibility(View.GONE); - Drawable drawable = contactAvatar.getDrawable(); - if (drawable == null) { - drawable = itemView.getContext().getDrawable(R.drawable.baseline_account_circle_24); + try { + this.id = String.valueOf(conversation.getThread_id()); + + int contactColor = Helpers.getColor(itemView.getContext(), id); + if (conversation.getContact_name() != null && !conversation.getContact_name().isEmpty()) { + this.contactAvatar.setVisibility(View.GONE); + this.contactInitials.setVisibility(View.VISIBLE); + this.contactInitials.setAvatarInitials(conversation.getContact_name().contains(" ") ? + conversation.getContact_name() : conversation.getContact_name().substring(0, 1)); + this.contactInitials.setAvatarInitialsBackgroundColor(contactColor); + } else { + this.contactAvatar.setVisibility(View.VISIBLE); + this.contactInitials.setVisibility(View.GONE); + Drawable drawable = contactAvatar.getDrawable(); + if (drawable == null) { + drawable = itemView.getContext().getDrawable(R.drawable.baseline_account_circle_24); + } + if (drawable != null) + drawable.setColorFilter(contactColor, PorterDuff.Mode.SRC_IN); + contactAvatar.setImageDrawable(drawable); } - if(drawable != null) - drawable.setColorFilter(contactColor, PorterDuff.Mode.SRC_IN); - contactAvatar.setImageDrawable(drawable); + if (conversation.getContact_name() != null) { + this.address.setText(conversation.getContact_name()); + } else this.address.setText(conversation.getAddress()); + + String text = conversation.getSnippet(); + + this.snippet.setText(text); + String date = Helpers.formatDate(itemView.getContext(), + Long.parseLong(conversation.getDate())); + this.date.setText(date); + this.materialCardView.setOnClickListener(onClickListener); + this.materialCardView.setOnLongClickListener(onLongClickListener); + + if (conversation.isIs_mute()) + this.muteAvatar.setVisibility(View.VISIBLE); + else + this.muteAvatar.setVisibility(View.GONE); + + // TODO: investigate new Avatar first before anything else + // this.contactInitials.setPlaceholder(itemView.getContext().getDrawable(R.drawable.round_person_24)); + } catch (Exception e) { + e.printStackTrace(); } - if(conversation.getContact_name() != null) { - this.address.setText(conversation.getContact_name()); - } - else this.address.setText(conversation.getAddress()); - - this.snippet.setText(conversation.getSnippet()); - String date = Helpers.formatDate(itemView.getContext(), - Long.parseLong(conversation.getDate())); - this.date.setText(date); - this.materialCardView.setOnClickListener(onClickListener); - this.materialCardView.setOnLongClickListener(onLongClickListener); - - if(conversation.isIs_mute()) - this.muteAvatar.setVisibility(View.VISIBLE); - else - this.muteAvatar.setVisibility(View.GONE); - - // TODO: investigate new Avatar first before anything else -// this.contactInitials.setPlaceholder(itemView.getContext().getDrawable(R.drawable.round_person_24)); } public static int getViewType(int position, List items) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java index aa1de2ad..6774336d 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java @@ -43,7 +43,7 @@ ConversationsThreadsEncryption.class, Conversation.class, GatewayClient.class}, - version = 12, autoMigrations = {@AutoMigration(from = 11, to = 12)}) + version = 13, autoMigrations = {@AutoMigration(from = 12, to = 13)}) public abstract class Datastore extends RoomDatabase { public static Datastore datastore; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java index 4d8ffff5..3c34d189 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java @@ -55,6 +55,14 @@ public static Cursor fetchByThreadId(Context context, String threadId) { null); } + public static Cursor fetchByAddress(Context context, String address) { + return context.getContentResolver().query(Telephony.Sms.CONTENT_URI, + null, + Telephony.Sms.ADDRESS + "=?", + new String[]{address}, + null); + } + public static Cursor fetchByMessageId(@NonNull Context context, String id) { return context.getContentResolver().query( Telephony.Sms.CONTENT_URI, diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSDatabaseWrapper.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSDatabaseWrapper.java index 0901525a..9434091d 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSDatabaseWrapper.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSDatabaseWrapper.java @@ -29,6 +29,14 @@ public static void send_text(Context context, Conversation conversation, Bundle // conversation.setThread_id(nativeOutputs[NativeSMSDB.THREAD_ID]); } + public static void send_text(Context context, Conversation conversation, String text, Bundle bundle) throws Exception { + String transmissionAddress = Helpers.getFormatForTransmission(conversation.getAddress(), + Helpers.getUserCountry(context)); + String[] nativeOutputs = NativeSMSDB.Outgoing._send_text(context, conversation.getMessage_id(), + transmissionAddress, text, conversation.getSubscription_id(), bundle); + + } + public static void saveDraft(Context context, Conversation conversation) { Log.d(SMSDatabaseWrapper.class.getName(), "Saving draft: " + conversation.getText()); String[] outputs = NativeSMSDB.Outgoing.register_drafts(context, conversation.getMessage_id(), diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 02dfb34c..c5b6d2e0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -8,7 +8,9 @@ import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.UNREAD_MESSAGE_TYPES; import androidx.annotation.NonNull; +import androidx.annotation.OptIn; import androidx.appcompat.app.ActionBar; +import androidx.compose.material3.CardKt; import androidx.core.app.NotificationManagerCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; @@ -37,6 +39,10 @@ import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.badge.BadgeDrawable; +import com.google.android.material.badge.BadgeUtils; +import com.google.android.material.badge.ExperimentalBadgeUtils; +import com.google.android.material.card.MaterialCardView; import com.google.android.material.navigation.NavigationView; import java.util.ArrayList; @@ -72,6 +78,7 @@ protected void onCreate(Bundle savedInstanceState) { configureNavigationBar(); } + @OptIn(markerClass = ExperimentalBadgeUtils.class) public void configureNavigationBar() { navigationView = findViewById(R.id.conversations_threads_navigation_view); View view = getLayoutInflater().inflate(R.layout.header_navigation_drawer, null); @@ -90,8 +97,6 @@ public void configureNavigationBar() { threadedConversationsViewModel.folderMetrics.observe(this, new Observer>() { @Override public void onChanged(List integers) { -// inboxMenuItem.setTitle(getString(R.string.conversations_navigation_view_inbox) -// + "(" + integers.get(0) + ")"); draftMenuItem.setTitle(getString(R.string.conversations_navigation_view_drafts) + "(" + integers.get(0) + ")"); @@ -120,6 +125,7 @@ public void onClick(View v) { navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { + drawerLayout.close(); String messageType = ""; String label = ""; String noContent = ""; @@ -186,7 +192,6 @@ else if(item.getItemId() == R.id.navigation_view_menu_muted) { ThreadedConversationsFragment.class, bundle, null) .setReorderingAllowed(true) .commit(); - drawerLayout.close(); return true; } }); diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionDao.java b/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionDao.java index f92145f8..1e6fc047 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionDao.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionDao.java @@ -1,6 +1,7 @@ package com.afkanerd.deku.E2EE; +import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; @@ -12,6 +13,9 @@ @Dao public interface ConversationsThreadsEncryptionDao { + @Query("SELECT * FROM ConversationsThreadsEncryption WHERE keystoreAlias = :keystoreAlias") + LiveData fetchLiveData(String keystoreAlias); + @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(ConversationsThreadsEncryption conversationsThreadsEncryption); diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java index 9d8c02eb..0f7901e8 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java @@ -1,45 +1,116 @@ package com.afkanerd.deku.E2EE; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.provider.ContactsContract; import android.provider.Telephony; import android.util.Base64; +import android.util.Log; import android.util.Pair; import android.view.MenuItem; import android.view.View; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.Observer; import com.afkanerd.deku.DefaultSMS.CustomAppCompactActivity; +import com.afkanerd.deku.DefaultSMS.Fragments.ModalSheetFragment; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.DefaultSMS.R; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.textfield.TextInputLayout; import com.google.i18n.phonenumbers.NumberParseException; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitOption; import java.security.GeneralSecurityException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; public class E2EECompactActivity extends CustomAppCompactActivity { protected ThreadedConversations threadedConversations; View securePopUpRequest; - - protected String keystoreAlias; + boolean isEncrypted = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } - boolean isEncrypted = false; + protected void attachObservers() { + try { + final String keystoreAlias = + E2EEHandler.deriveKeystoreAlias(threadedConversations.getAddress(), 0); + databaseConnector.conversationsThreadsEncryptionDao().fetchLiveData(keystoreAlias) + .observe(this, new Observer() { + @Override + public void onChanged(ConversationsThreadsEncryption conversationsThreadsEncryption) { + if(conversationsThreadsEncryption != null && + conversationsThreadsEncryption.getKeystoreAlias() + .equals(keystoreAlias)) { + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + try { + final boolean isSelf = E2EEHandler + .isSelf(getApplicationContext(), keystoreAlias); + + threadedConversations.setSelf(isSelf); + + String _keystoreAlias = threadedConversations.isSelf() ? + E2EEHandler.buildForSelf(keystoreAlias) : + keystoreAlias; + + if(E2EEHandler.canCommunicateSecurely(getApplicationContext(), + _keystoreAlias, true)) { + threadedConversations.setIs_secured(true); + Log.d(getClass().getName(), "Thread at activity changed to secured"); + informSecured(true); + } + else { + showSecureRequestAgreementModal(); + } + } catch (CertificateException | KeyStoreException | + NoSuchAlgorithmException | IOException | + UnrecoverableEntryException | InterruptedException e) { + e.printStackTrace(); + } + } + }); + } + else { + informSecured(false); + } + } + }); + } catch (NumberParseException e) { + e.printStackTrace(); + } + } + /** * @@ -60,12 +131,16 @@ public void sendTextMessage(final String text, int subscriptionId, @Override public void run() { try { + String keystoreAlias = + E2EEHandler.deriveKeystoreAlias( + threadedConversations.getAddress(), 0); byte[][] cipherText = E2EEHandler.encrypt(getApplicationContext(), - keystoreAlias, text.getBytes(StandardCharsets.UTF_8)); - String text = E2EEHandler.buildTransmissionText(cipherText[0]); + keystoreAlias, text.getBytes(StandardCharsets.UTF_8), + threadedConversations.isSelf()); + String encryptedText = E2EEHandler.buildTransmissionText(cipherText[0]); isEncrypted = true; - sendTextMessage(text, subscriptionId, threadedConversations, messageId, - cipherText[1]); + sendTextMessage(encryptedText, subscriptionId, threadedConversations, + messageId, cipherText[1]); } catch (Throwable e) { e.printStackTrace(); } @@ -80,16 +155,18 @@ public void run() { @Override public void informSecured(boolean secured) { + TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); runOnUiThread(new Runnable() { @Override public void run() { - threadedConversations.is_secured = secured; if(secured && securePopUpRequest != null) { securePopUpRequest.setVisibility(View.GONE); - TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); layout.setPlaceholderText(getString(R.string.send_message_secured_text_box_hint)); + getSupportActionBar().setSubtitle(R.string.messages_thread_encrypted_content_label); + } else { + layout.setPlaceholderText(getString(R.string.send_message_text_box_hint)); + getSupportActionBar().setSubtitle(null); } - } }); } @@ -102,7 +179,7 @@ public void run() { try { Pair transmissionRequestKeyPair = E2EEHandler.buildForEncryptionRequest(getApplicationContext(), - threadedConversations.getAddress()); + threadedConversations.getAddress(), null); final String messageId = String.valueOf(System.currentTimeMillis()); Conversation conversation = new Conversation(); @@ -117,7 +194,9 @@ public void run() { conversation.setDate(String.valueOf(System.currentTimeMillis())); conversation.setStatus(Telephony.Sms.STATUS_PENDING); - long id = conversationsViewModel.insert(getApplicationContext(), conversation); +// Log.d(getClass().getName(), "Threaded conversation safe: " + +// threadedConversations.isIs_secured()); + long id = conversationsViewModel.insert(conversation); SMSDatabaseWrapper.send_data(getApplicationContext(), conversation); } catch (Exception e) { e.printStackTrace(); @@ -128,11 +207,12 @@ public void run() { } private void showSecureRequestPopUpMenu() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.conversation_secure_popup_request_menu_title)); + View conversationSecurePopView = getLayoutInflater() + .inflate(R.layout.conversation_secure_popup_menu, null); - View conversationSecurePopView = View.inflate(getApplicationContext(), - R.layout.conversation_secure_popup_menu, null); +// AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_main); + AlertDialog.Builder builder = new AlertDialog.Builder(conversationSecurePopView.getContext()); + builder.setTitle(getString(R.string.conversation_secure_popup_request_menu_title)); builder.setView(conversationSecurePopView); Button yesButton = conversationSecurePopView.findViewById(R.id.conversation_secure_popup_menu_send); @@ -181,8 +261,6 @@ protected void onStart() { super.onStart(); securePopUpRequest = findViewById(R.id.conversations_request_secure_pop_layout); setSecurePopUpRequest(); -// if(!SettingsHandler.alertNotEncryptedCommunicationDisabled(getApplicationContext())) -// securePopUpRequest.setVisibility(View.VISIBLE); } @Override @@ -197,31 +275,69 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { return super.onOptionsItemSelected(item); } - @Override - protected void onResume() { - super.onResume(); - if(threadedConversations != null) { - ThreadingPoolExecutor.executorService.execute(new Runnable() { + private void showSecureRequestAgreementModal() { + Fragment fragment = getSupportFragmentManager() + .findFragmentByTag(ModalSheetFragment.TAG); + if(threadedConversations != null && (fragment == null || !fragment.isAdded())) { + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + ModalSheetFragment modalSheetFragment = new ModalSheetFragment(threadedConversations); + fragmentTransaction.add(modalSheetFragment, + ModalSheetFragment.TAG); + fragmentTransaction.show(modalSheetFragment); + runOnUiThread(new Runnable() { @Override public void run() { - try { - keystoreAlias = E2EEHandler.deriveKeystoreAlias(threadedConversations.getAddress(), 0); - threadedConversations.is_secured = - E2EEHandler.canCommunicateSecurely(getApplicationContext(), keystoreAlias); - if(threadedConversations.is_secured) { - runOnUiThread(new Runnable() { + fragmentTransaction.commitNow(); + Log.d(getClass().getName(), "Fragment null: " + + String.valueOf(modalSheetFragment.getView() == null)); + modalSheetFragment.getView().findViewById(R.id.conversation_secure_request_agree_btn) + .setOnClickListener(new View.OnClickListener() { @Override - public void run() { - TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); - layout.setPlaceholderText(getString(R.string.send_message_secured_text_box_hint)); + public void onClick(View v) { + modalSheetFragment.dismiss(); + agreeToSecure(); } }); - } - } catch (IOException | GeneralSecurityException | NumberParseException e) { - e.printStackTrace(); - } } }); +// Fragment fragment = getSupportFragmentManager() +// .findFragmentByTag(ModalSheetFragment.TAG); +// if(fragment == null || !fragment.isAdded()) { +// modalSheetFragment.show(getSupportFragmentManager(), ModalSheetFragment.TAG); +// Log.d(getClass().getName(), "Fragment null: " + String.valueOf(modalSheetFragment._view == null)); +// } } } + + private void agreeToSecure() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + try { + String keystoreAlias = E2EEHandler + .deriveKeystoreAlias(threadedConversations.getAddress(), 0); + if (threadedConversations.isSelf()) { + keystoreAlias = E2EEHandler.buildForSelf(keystoreAlias); + Pair keystorePair = E2EEHandler + .buildForEncryptionRequest(getApplicationContext(), + threadedConversations.getAddress(), keystoreAlias); + + byte[] transmissionKey = E2EEHandler + .extractTransmissionKey(keystorePair.second); + if (threadedConversations.isSelf()) + E2EEHandler.insertNewAgreementKeyDefault(getApplicationContext(), + transmissionKey, keystoreAlias); + threadedConversations.setIs_secured(true); + threadedConversations.setSelf(true); + Datastore.datastore.threadedConversationsDao().update(threadedConversations); + } else + sendDataMessage(threadedConversations); + } catch(Exception e) { + e.printStackTrace(); + } + } + }); + } + } diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java index 548a0c8a..cd81a51b 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java @@ -39,6 +39,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; import java.util.Map; import javax.crypto.BadPaddingException; @@ -80,6 +81,7 @@ public static String deriveKeystoreAlias(String address, int mode) throws Number } public static String getAddressFromKeystore(String keystoreAlias) { + keystoreAlias = buildForOriginal(keystoreAlias); String decodedAlias = new String(Base64.decode(keystoreAlias, Base64.NO_WRAP), StandardCharsets.UTF_8); return "+" + decodedAlias.split("_")[0]; @@ -93,16 +95,62 @@ public static boolean isAvailableInKeystore(String keystoreAlias) throws Certifi return KeystoreHelpers.isAvailableInKeystore(keystoreAlias); } - public static boolean canCommunicateSecurely(Context context, String keystoreAlias) throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + public static boolean samePublicKey(Context context, String keystoreAlias, byte[] publicKey) throws UnrecoverableEntryException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, InterruptedException { + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + ConversationsThreadsEncryption conversationsThreadsEncryption = + Datastore.datastore.conversationsThreadsEncryptionDao().fetch(keystoreAlias); + if(conversationsThreadsEncryption == null) + return false; + + byte[] currentPubKey = + Base64.decode(conversationsThreadsEncryption.getPublicKey(), Base64.NO_WRAP); + return Arrays.equals(currentPubKey, publicKey); + } + + public static boolean isSelf(Context context, String keystoreAlias) throws UnrecoverableEntryException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, InterruptedException { + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + + ConversationsThreadsEncryption conversationsThreadsEncryption = + Datastore.datastore.conversationsThreadsEncryptionDao().fetch(keystoreAlias); + + KeyPair keyPair = getKeyPairBasedVersioning(context, keystoreAlias); + + if(conversationsThreadsEncryption == null || keyPair == null) + return false; + + byte[] currentPubKey = + Base64.decode(conversationsThreadsEncryption.getPublicKey(), Base64.NO_WRAP); + return Arrays.equals(currentPubKey, keyPair.getPublic().getEncoded()); + } + + public static boolean canCommunicateSecurely(Context context, String keystoreAlias, boolean strict) throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { Datastore.datastore = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .enableMultiInstanceInvalidation() .build(); } - return isAvailableInKeystore(keystoreAlias) && + if(strict) + return isAvailableInKeystore(keystoreAlias) && + Datastore.datastore.conversationsThreadsEncryptionDao() + .findByKeystoreAlias(keystoreAlias) != null; + + return (isAvailableInKeystore(keystoreAlias) && Datastore.datastore.conversationsThreadsEncryptionDao() - .findByKeystoreAlias(keystoreAlias) != null; + .findByKeystoreAlias(keystoreAlias) != null || + isAvailableInKeystore(keystoreAlias) && + Datastore.datastore.conversationsThreadsEncryptionDao() + .findByKeystoreAlias(buildForSelf(keystoreAlias)) != null); } public static PublicKey createNewKeyPair(Context context, String keystoreAlias) @@ -239,6 +287,9 @@ public static byte[] extractTransmissionKey(byte[] data) { /** * This uses session = 0, which is the default PublicKey values for. * + * If the PublicKey in Keystore is the same as ConversationsEncryption database for the same alias, + * this infers same person making a request and therefore uses the same public key. + * * @param context * @param address * @return @@ -247,13 +298,24 @@ public static byte[] extractTransmissionKey(byte[] data) { * @throws IOException * @throws InterruptedException */ - public static Pair buildForEncryptionRequest(Context context, String address) throws Exception { + public static Pair buildForEncryptionRequest(Context context, String address, + String keystoreAlias) throws Exception { int session = 0; - String keystoreAlias = deriveKeystoreAlias(address, session); + + if(keystoreAlias == null) + keystoreAlias = deriveKeystoreAlias(address, session); PublicKey publicKey = createNewKeyPair(context, keystoreAlias); return new Pair<>(keystoreAlias, buildDefaultPublicKey(publicKey.getEncoded())); } + public static String buildForSelf(String keystoreAlias) { + return keystoreAlias + "_self"; + } + + public static String buildForOriginal(String keystoreAlias) { + return keystoreAlias.endsWith("_self") ? keystoreAlias.split("_")[0] : keystoreAlias; + } + /** * Inserts the peer public key which would be used as the primary key for everything this peer. * @param context @@ -296,6 +358,20 @@ public static ConversationsThreadsEncryption fetchStoredPeerData(Context context return conversationsThreadsEncryptionDao.fetch(keystoreAlias); } + /** + * Get the default KeyPair, used in identifying the peers. This is different from the keypairs + * stored during ratcheting. + * + * @param context + * @param keystoreAlias + * @return + * @throws UnrecoverableEntryException + * @throws CertificateException + * @throws KeyStoreException + * @throws IOException + * @throws NoSuchAlgorithmException + * @throws InterruptedException + */ public static KeyPair getKeyPairBasedVersioning(Context context, String keystoreAlias) throws UnrecoverableEntryException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, InterruptedException { final KeyPair[] keyPair = new KeyPair[1]; if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { @@ -334,17 +410,27 @@ protected static String getKeystoreForRatchets(String keystoreAlias) { return keystoreAlias + "-ratchet-sessions"; } - public static byte[][] encrypt(Context context, final String keystoreAlias, byte[] data) throws Throwable { + /** + * This returns a header, ciphertext and the mk. + * + * @param context + * @param keystoreAlias + * @param data + * @return + * @throws Throwable + */ + public static byte[][] encrypt(Context context, final String keystoreAlias, byte[] data, + boolean isSelf) throws Throwable { if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), Datastore.class, Datastore.databaseName) .enableMultiInstanceInvalidation() .build(); } - ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = Datastore.datastore.conversationsThreadsEncryptionDao(); - ConversationsThreadsEncryption conversationsThreadsEncryption = + ConversationsThreadsEncryption conversationsThreadsEncryption = isSelf ? + conversationsThreadsEncryptionDao.findByKeystoreAlias(buildForSelf(keystoreAlias)): conversationsThreadsEncryptionDao.findByKeystoreAlias(keystoreAlias); States states; @@ -356,7 +442,7 @@ public static byte[][] encrypt(Context context, final String keystoreAlias, byte * You are Alice, so act like it */ PublicKey publicKey = SecurityECDH.buildPublicKey( - Base64.decode(conversationsThreadsEncryption.getPublicKey(), Base64.DEFAULT)); + Base64.decode(conversationsThreadsEncryption.getPublicKey(), Base64.NO_WRAP)); keyPair = getKeyPairBasedVersioning(context, keystoreAlias); final byte[] SK = SecurityECDH.generateSecretKey(keyPair, publicKey); @@ -380,25 +466,31 @@ public static byte[][] encrypt(Context context, final String keystoreAlias, byte cipherPair.second[1]}; } - public static byte[] decrypt(Context context, final String keystoreAlias, - final byte[] cipherText, byte[] mk, byte[] _AD) throws Throwable { + public static byte[] decrypt(Context context, final String keystoreAlias, final byte[] cipherText, + byte[] mk, byte[] _AD, boolean isSelf) throws Throwable { if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), Datastore.class, Datastore.databaseName) .enableMultiInstanceInvalidation() .build(); } + if(isSelf && !keystoreAlias.endsWith("_self")) + throw new Exception("Expected " + keystoreAlias + "_self but got " + keystoreAlias); + + Log.d(E2EEHandler.class.getName(), "Is self: " + isSelf + ":" + keystoreAlias); ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = Datastore.datastore.conversationsThreadsEncryptionDao(); ConversationsThreadsEncryption conversationsThreadsEncryption = - conversationsThreadsEncryptionDao.findByKeystoreAlias(keystoreAlias); + conversationsThreadsEncryptionDao.findByKeystoreAlias(isSelf ? + buildForOriginal(keystoreAlias) : + keystoreAlias); - Headers header = new Headers(); - byte[] outputCipherText = header.deSerializeHeader(cipherText); + String keystoreAliasRatchet = getKeystoreForRatchets(keystoreAlias); States states; - final String keystoreAliasRatchet = getKeystoreForRatchets(keystoreAlias); + Headers header = new Headers(); + byte[] outputCipherText = header.deSerializeHeader(cipherText); KeyPair keyPair = getKeyPairBasedVersioning(context, keystoreAliasRatchet); if(keyPair == null) { /** @@ -406,12 +498,14 @@ public static byte[] decrypt(Context context, final String keystoreAlias, */ keyPair = getKeyPairBasedVersioning(context, keystoreAlias); PublicKey publicKey = SecurityECDH.buildPublicKey( - Base64.decode(conversationsThreadsEncryption.getPublicKey(), Base64.DEFAULT)); + Base64.decode(conversationsThreadsEncryption.getPublicKey(), Base64.NO_WRAP)); final byte[] SK = SecurityECDH.generateSecretKey(keyPair, publicKey); states = Ratchets.ratchetInitBob(new States(), SK, keyPair); } else { + Log.d(E2EEHandler.class.getName(), "Yep not null no more..."); states = new States(keyPair, conversationsThreadsEncryption.getStates()); + Log.d(E2EEHandler.class.getName(), states.getSerializedStates()); } byte[] AD = _AD == null ? @@ -476,10 +570,13 @@ public static void clear(Context context, String keystoreAlias) throws Certifica .build(); } removeFromKeystore(context, keystoreAlias); + removeFromKeystore(context, buildForSelf(keystoreAlias)); removeFromKeystore(context, getKeystoreForRatchets(keystoreAlias)); + removeFromKeystore(context, getKeystoreForRatchets(buildForSelf(keystoreAlias))); ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = Datastore.datastore.conversationsThreadsEncryptionDao(); conversationsThreadsEncryptionDao.delete(keystoreAlias); + conversationsThreadsEncryptionDao.delete(buildForSelf(keystoreAlias)); conversationsThreadsEncryptionDao.delete(getKeystoreForRatchets(keystoreAlias)); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java index 493cdd9b..f5c55b0e 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java @@ -9,6 +9,7 @@ import androidx.room.Room; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import java.util.ArrayList; import java.util.HashSet; @@ -18,10 +19,17 @@ public class GatewayClientProjectListingViewModel extends ViewModel { long id; + MutableLiveData> mutableLiveData = new MutableLiveData<>(); public LiveData> get(Datastore databaseConnector, long id) { this.id = id; GatewayClientProjectDao gatewayClientProjectDao = databaseConnector.gatewayClientProjectDao(); - return gatewayClientProjectDao.fetchGatewayClientId(id); + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + mutableLiveData.postValue(gatewayClientProjectDao.fetchGatewayClientIdList(id)); + } + }); + return mutableLiveData; } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 324b52b0..4b286fd5 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -36,6 +36,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; @@ -257,7 +258,7 @@ private DeliverCallback getDeliverCallback(final Channel channel, final int subs conversation.setThread_id(String.valueOf(threadId)); conversation.setStatus(Telephony.Sms.STATUS_PENDING); - databaseConnector.conversationDao().insert(conversation); + databaseConnector.threadedConversationsDao().insertThreadAndConversation(conversation); Log.d(getClass().getName(), "Sending RMQ SMS: " + subscriptionId + ":" + conversation.getAddress()); SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 5c2572b3..dc81aca2 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -102,7 +102,6 @@ android:layout_marginTop="8dp" android:fontFamily="@font/roboto_bold" android:text="@string/about_deku_github_btn" - android:textColor="@color/colorSecondary" app:layout_constraintStart_toStartOf="@+id/textView11" app:layout_constraintTop_toBottomOf="@+id/textView11" /> diff --git a/app/src/main/res/layout/activity_compose_new_message.xml b/app/src/main/res/layout/activity_compose_new_message.xml index 3ff81cff..839883f0 100644 --- a/app/src/main/res/layout/activity_compose_new_message.xml +++ b/app/src/main/res/layout/activity_compose_new_message.xml @@ -14,13 +14,15 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> + diff --git a/app/src/main/res/layout/activity_conversations.xml b/app/src/main/res/layout/activity_conversations.xml index 0be010a4..ef71b414 100644 --- a/app/src/main/res/layout/activity_conversations.xml +++ b/app/src/main/res/layout/activity_conversations.xml @@ -74,9 +74,9 @@ + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_conversations_threads.xml b/app/src/main/res/layout/activity_conversations_threads.xml index 385d8c0c..0a96df93 100644 --- a/app/src/main/res/layout/activity_conversations_threads.xml +++ b/app/src/main/res/layout/activity_conversations_threads.xml @@ -48,7 +48,6 @@ android:textAlignment="center" app:elevation="10dp" app:icon="@drawable/bubble_round_speech" - app:iconTint="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/activity_default_check.xml b/app/src/main/res/layout/activity_default_check.xml index 6e0e7a7c..5d78bd14 100644 --- a/app/src/main/res/layout/activity_default_check.xml +++ b/app/src/main/res/layout/activity_default_check.xml @@ -41,10 +41,8 @@ android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="156dp" - android:background="@drawable/compose_message_drawable" android:text="@string/default_check_btn_text" android:textAllCaps="false" - android:textColor="@color/white" android:textSize="14sp" app:layout_constraintBottom_toTopOf="@+id/textView3" app:layout_constraintEnd_toEndOf="parent" @@ -57,7 +55,6 @@ android:layout_marginBottom="25dp" android:onClick="clickPrivacyPolicy" android:text="@string/default_check_privacy_policy" - android:textColor="@color/colorSecondary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/activity_gateway_client_add.xml b/app/src/main/res/layout/activity_gateway_client_add.xml index d51af235..fe5813b3 100644 --- a/app/src/main/res/layout/activity_gateway_client_add.xml +++ b/app/src/main/res/layout/activity_gateway_client_add.xml @@ -346,10 +346,8 @@ android:layout_height="wrap_content" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" - android:background="@drawable/compose_message_drawable" android:text="@string/save_new_gateway_client" android:textAllCaps="false" - android:textColor="@color/colorSecondary" android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/new_gateway_client_input_constraint" diff --git a/app/src/main/res/layout/activity_gateway_client_customization.xml b/app/src/main/res/layout/activity_gateway_client_customization.xml index b04f0ada..5a5ff9bd 100644 --- a/app/src/main/res/layout/activity_gateway_client_customization.xml +++ b/app/src/main/res/layout/activity_gateway_client_customization.xml @@ -166,10 +166,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" - android:background="@drawable/compose_message_drawable" android:text="@string/save_new_gateway_client" android:textAllCaps="false" - android:textColor="@color/colorSecondary" android:textSize="14sp" app:layout_constraintEnd_toEndOf="@+id/new_gateway_client_project_binding_sim_2_constraint" app:layout_constraintStart_toStartOf="@+id/new_gateway_client_project_binding_sim_2_constraint" diff --git a/app/src/main/res/layout/activity_gateway_server_add.xml b/app/src/main/res/layout/activity_gateway_server_add.xml index 5fbe50bd..49b6caaf 100644 --- a/app/src/main/res/layout/activity_gateway_server_add.xml +++ b/app/src/main/res/layout/activity_gateway_server_add.xml @@ -153,7 +153,6 @@ android:background="@drawable/compose_message_drawable" android:text="@string/save_new_gateway_client" android:textAllCaps="false" - android:textColor="@color/colorSecondary" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/conversation_compose_layout.xml b/app/src/main/res/layout/conversation_compose_layout.xml index 48e8e804..4ab993dd 100644 --- a/app/src/main/res/layout/conversation_compose_layout.xml +++ b/app/src/main/res/layout/conversation_compose_layout.xml @@ -56,7 +56,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="16dp" - android:textColor="@color/colorSecondary" android:textSize="10sp" android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/conversation_secure_popup_menu.xml b/app/src/main/res/layout/conversation_secure_popup_menu.xml index b1af58f1..129de8d1 100644 --- a/app/src/main/res/layout/conversation_secure_popup_menu.xml +++ b/app/src/main/res/layout/conversation_secure_popup_menu.xml @@ -1,5 +1,6 @@ - - - + app:layout_constraintTop_toTopOf="parent"> -