From e0fe72ac5b82941ff4ddf02be46c4acd8115d6ff Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Thu, 19 Mar 2026 12:06:14 +0200 Subject: [PATCH 01/15] Remove most of `.idea` from Git --- .gitignore | 9 ++------- .idea/.gitignore | 3 --- .idea/compiler.xml | 6 ------ .idea/deploymentTargetSelector.xml | 10 ---------- .idea/gradle.xml | 23 ----------------------- .idea/kotlinc.xml | 6 ------ .idea/misc.xml | 7 ------- 7 files changed, 2 insertions(+), 62 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/deploymentTargetSelector.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/kotlinc.xml delete mode 100644 .idea/misc.xml diff --git a/.gitignore b/.gitignore index 958eea26..fb261f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,8 @@ /releases/ .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -/.idea/deploymentTargetDropDown.xml +/.idea/* +!/.idea/vcs.xml .DS_Store /build /captures diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56e..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index b268ef36..00000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 2e12cc38..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index bb449370..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 9218e2b5..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file From 5cc2355c6c01fe857e378eef1976ff9dfb16b0f6 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Fri, 20 Mar 2026 10:40:05 +0200 Subject: [PATCH 02/15] Update dependencies versions --- app/build.gradle.kts | 6 +- build.gradle.kts | 6 +- gradle/verification-metadata.xml | 1299 ++++++++++++---------- gradle/wrapper/gradle-wrapper.properties | 4 +- 4 files changed, 701 insertions(+), 614 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7338059e..84d0ecdb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -95,9 +95,9 @@ dependencies { implementation("androidx.preference:preference:1.2.1") implementation("androidx.palette:palette:1.0.0") implementation("androidx.recyclerview:recyclerview:1.4.0") - implementation("com.github.bumptech.glide:glide:5.0.4") - implementation("com.google.guava:guava:33.4.8-android") - implementation("com.googlecode.libphonenumber:libphonenumber:8.13.52") + implementation("com.github.bumptech.glide:glide:5.0.5") + implementation("com.google.guava:guava:33.5.0-android") + implementation("com.googlecode.libphonenumber:libphonenumber:9.0.26") implementation("com.google.code.findbugs:jsr305:3.0.2") implementation(project(":lib:platform_frameworks_opt_chips")) diff --git a/build.gradle.kts b/build.gradle.kts index 137ace6e..632c4f0b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - id("com.android.application") version "9.0.0" apply false + id("com.android.application") version "9.1.0" apply false } buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0") - classpath("com.google.devtools.ksp:symbol-processing-gradle-plugin:2.3.5") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.20") + classpath("com.google.devtools.ksp:symbol-processing-gradle-plugin:2.3.6") } } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 3a087bb7..d50792af 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -258,20 +258,20 @@ - - + + - - + + - - - + + + - - + + @@ -666,185 +666,177 @@ - - + + - - + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - - - - - - - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + - - + + @@ -855,44 +847,44 @@ - - - + + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - + + @@ -911,284 +903,228 @@ - - - + + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + + - - + + - - - + + + - - + + - - - - - - - - - - - - - - - - - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - + + @@ -1309,6 +1245,14 @@ + + + + + + + + @@ -1333,28 +1277,33 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + + + + + + @@ -1386,12 +1335,17 @@ - - - + + + - - + + + + + + + @@ -1414,9 +1368,9 @@ - - - + + + @@ -1467,12 +1421,12 @@ - - - + + + - - + + @@ -1485,21 +1439,11 @@ - - - - - - - - - - @@ -1532,6 +1476,14 @@ + + + + + + + + @@ -1550,22 +1502,19 @@ - - - - - - - - + + + - - + + + + @@ -1576,6 +1525,19 @@ + + + + + + + + + + + + + @@ -1584,14 +1546,6 @@ - - - - - - - - @@ -1600,12 +1554,20 @@ - - - + + + + + + + + + + + - - + + @@ -1613,62 +1575,67 @@ - - - - - - - - + + + + + + + + + + + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -1679,17 +1646,17 @@ - - - + + + - - + + - - - + + + @@ -1836,14 +1803,6 @@ - - - - - - - - @@ -2392,14 +2351,6 @@ - - - - - - - - @@ -2467,6 +2418,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2491,148 +2479,156 @@ - - - + + + + + + + + + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -2667,12 +2663,12 @@ - - - + + + - - + + @@ -2707,6 +2703,14 @@ + + + + + + + + @@ -2728,9 +2732,9 @@ - - - + + + @@ -2781,36 +2785,36 @@ - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -2849,11 +2853,6 @@ - - - - - @@ -2915,25 +2914,16 @@ - - - - - - - - - @@ -2951,44 +2941,49 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + + + + + + - - - + + + - - + + - - - + + + - - + + @@ -3022,5 +3017,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 92ed9434..8e61ef12 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=60ea723356d81263e8002fec0fcf9e2b0eee0c0850c7a3d7ab0a63f2ccc601f3 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From f340a91507ab8344983a9987ba77daee265685e1 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Fri, 20 Mar 2026 10:40:24 +0200 Subject: [PATCH 03/15] Add sources and javadoc artifacts verification --- gradle/verification-metadata.xml | 582 +++++++++++++++++++++++++++++++ 1 file changed, 582 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d50792af..d2dde522 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -12,6 +12,9 @@ + + + @@ -20,6 +23,9 @@ + + + @@ -33,6 +39,9 @@ + + + @@ -43,6 +52,9 @@ + + + @@ -56,6 +68,9 @@ + + + @@ -72,6 +87,9 @@ + + + @@ -80,6 +98,9 @@ + + + @@ -88,6 +109,9 @@ + + + @@ -96,6 +120,9 @@ + + + @@ -104,6 +131,9 @@ + + + @@ -117,6 +147,9 @@ + + + @@ -125,6 +158,9 @@ + + + @@ -133,6 +169,9 @@ + + + @@ -146,6 +185,9 @@ + + + @@ -159,6 +201,9 @@ + + + @@ -167,6 +212,9 @@ + + + @@ -175,6 +223,9 @@ + + + @@ -188,6 +239,9 @@ + + + @@ -196,6 +250,9 @@ + + + @@ -209,6 +266,9 @@ + + + @@ -217,6 +277,9 @@ + + + @@ -225,6 +288,9 @@ + + + @@ -233,6 +299,9 @@ + + + @@ -249,6 +318,9 @@ + + + @@ -257,6 +329,9 @@ + + + @@ -265,6 +340,9 @@ + + + @@ -273,6 +351,9 @@ + + + @@ -289,6 +370,9 @@ + + + @@ -297,6 +381,9 @@ + + + @@ -310,6 +397,9 @@ + + + @@ -318,6 +408,9 @@ + + + @@ -326,6 +419,9 @@ + + + @@ -334,6 +430,9 @@ + + + @@ -347,6 +446,9 @@ + + + @@ -355,6 +457,9 @@ + + + @@ -376,6 +481,9 @@ + + + @@ -402,6 +510,9 @@ + + + @@ -420,6 +531,9 @@ + + + @@ -438,6 +552,9 @@ + + + @@ -451,6 +568,9 @@ + + + @@ -464,6 +584,9 @@ + + + @@ -477,6 +600,9 @@ + + + @@ -495,6 +621,9 @@ + + + @@ -508,6 +637,9 @@ + + + @@ -516,6 +648,9 @@ + + + @@ -532,6 +667,9 @@ + + + @@ -540,6 +678,9 @@ + + + @@ -556,6 +697,9 @@ + + + @@ -564,6 +708,9 @@ + + + @@ -572,6 +719,9 @@ + + + @@ -580,6 +730,9 @@ + + + @@ -588,6 +741,9 @@ + + + @@ -596,6 +752,9 @@ + + + @@ -609,6 +768,9 @@ + + + @@ -617,6 +779,9 @@ + + + @@ -625,6 +790,9 @@ + + + @@ -633,6 +801,9 @@ + + + @@ -641,6 +812,9 @@ + + + @@ -649,6 +823,9 @@ + + + @@ -657,6 +834,9 @@ + + + @@ -665,6 +845,9 @@ + + + @@ -673,6 +856,9 @@ + + + @@ -681,6 +867,9 @@ + + + @@ -694,6 +883,9 @@ + + + @@ -702,6 +894,9 @@ + + + @@ -710,6 +905,9 @@ + + + @@ -718,6 +916,9 @@ + + + @@ -726,6 +927,9 @@ + + + @@ -734,6 +938,9 @@ + + + @@ -742,6 +949,9 @@ + + + @@ -750,6 +960,9 @@ + + + @@ -758,6 +971,9 @@ + + + @@ -766,6 +982,9 @@ + + + @@ -774,6 +993,9 @@ + + + @@ -790,6 +1012,9 @@ + + + @@ -798,6 +1023,9 @@ + + + @@ -806,6 +1034,9 @@ + + + @@ -814,6 +1045,9 @@ + + + @@ -822,6 +1056,9 @@ + + + @@ -830,6 +1067,9 @@ + + + @@ -838,6 +1078,9 @@ + + + @@ -854,6 +1097,9 @@ + + + @@ -862,6 +1108,9 @@ + + + @@ -870,6 +1119,9 @@ + + + @@ -878,6 +1130,9 @@ + + + @@ -886,6 +1141,9 @@ + + + @@ -894,6 +1152,9 @@ + + + @@ -902,6 +1163,9 @@ + + + @@ -910,6 +1174,9 @@ + + + @@ -926,6 +1193,9 @@ + + + @@ -934,6 +1204,9 @@ + + + @@ -942,6 +1215,9 @@ + + + @@ -1086,6 +1362,9 @@ + + + @@ -1102,6 +1381,9 @@ + + + @@ -1110,6 +1392,12 @@ + + + + + + @@ -1118,6 +1406,12 @@ + + + + + + @@ -1126,6 +1420,12 @@ + + + + + + @@ -1192,6 +1492,9 @@ + + + @@ -1205,6 +1508,12 @@ + + + + + + @@ -1221,6 +1530,9 @@ + + + @@ -1260,6 +1572,9 @@ + + + @@ -1268,6 +1583,9 @@ + + + @@ -1284,6 +1602,9 @@ + + + @@ -1292,6 +1613,9 @@ + + + @@ -1300,6 +1624,9 @@ + + + @@ -1326,6 +1653,9 @@ + + + @@ -1342,6 +1672,12 @@ + + + + + + @@ -1380,6 +1716,9 @@ + + + @@ -1396,6 +1735,9 @@ + + + @@ -1404,6 +1746,12 @@ + + + + + + @@ -1420,6 +1768,9 @@ + + + @@ -1451,6 +1802,12 @@ + + + + + + @@ -1475,6 +1832,9 @@ + + + @@ -1483,6 +1843,12 @@ + + + + + + @@ -1491,6 +1857,9 @@ + + + @@ -1524,6 +1893,9 @@ + + + @@ -1553,6 +1925,9 @@ + + + @@ -1645,6 +2020,9 @@ + + + @@ -1653,6 +2031,12 @@ + + + + + + @@ -1666,6 +2050,9 @@ + + + @@ -1674,6 +2061,9 @@ + + + @@ -1692,6 +2082,9 @@ + + + @@ -1720,6 +2113,9 @@ + + + @@ -1733,6 +2129,9 @@ + + + @@ -1741,6 +2140,9 @@ + + + @@ -1749,6 +2151,9 @@ + + + @@ -2108,6 +2513,9 @@ + + + @@ -2116,6 +2524,9 @@ + + + @@ -2137,6 +2548,9 @@ + + + @@ -2150,6 +2564,9 @@ + + + @@ -2158,6 +2575,9 @@ + + + @@ -2166,6 +2586,9 @@ + + + @@ -2174,6 +2597,9 @@ + + + @@ -2212,6 +2638,9 @@ + + + @@ -2220,6 +2649,9 @@ + + + @@ -2248,6 +2680,9 @@ + + + @@ -2289,6 +2724,9 @@ + + + @@ -2297,6 +2735,9 @@ + + + @@ -2305,6 +2746,9 @@ + + + @@ -2313,6 +2757,9 @@ + + + @@ -2321,6 +2768,9 @@ + + + @@ -2329,6 +2779,9 @@ + + + @@ -2350,6 +2803,9 @@ + + + @@ -2409,6 +2865,9 @@ + + + @@ -2417,6 +2876,9 @@ + + + @@ -2462,6 +2924,9 @@ + + + @@ -2470,6 +2935,12 @@ + + + + + + @@ -2486,6 +2957,9 @@ + + + @@ -2494,6 +2968,9 @@ + + + @@ -2502,6 +2979,9 @@ + + + @@ -2510,6 +2990,9 @@ + + + @@ -2550,6 +3033,9 @@ + + + @@ -2558,6 +3044,9 @@ + + + @@ -2574,6 +3063,9 @@ + + + @@ -2582,6 +3074,9 @@ + + + @@ -2590,6 +3085,9 @@ + + + @@ -2598,6 +3096,9 @@ + + + @@ -2606,6 +3107,9 @@ + + + @@ -2622,6 +3126,9 @@ + + + @@ -2630,6 +3137,9 @@ + + + @@ -2662,6 +3172,9 @@ + + + @@ -2702,6 +3215,9 @@ + + + @@ -2710,6 +3226,9 @@ + + + @@ -2744,6 +3263,12 @@ + + + + + + @@ -2760,6 +3285,9 @@ + + + @@ -2768,6 +3296,12 @@ + + + + + + @@ -2784,6 +3318,9 @@ + + + @@ -2792,6 +3329,9 @@ + + + @@ -2800,6 +3340,9 @@ + + + @@ -2808,6 +3351,9 @@ + + + @@ -2816,6 +3362,9 @@ + + + @@ -2904,6 +3453,9 @@ + + + @@ -2912,6 +3464,12 @@ + + + + + + @@ -2935,6 +3493,9 @@ + + + @@ -2948,6 +3509,9 @@ + + + @@ -2956,6 +3520,9 @@ + + + @@ -2969,6 +3536,9 @@ + + + @@ -2977,6 +3547,9 @@ + + + @@ -2985,6 +3558,9 @@ + + + @@ -2993,6 +3569,9 @@ + + + @@ -3016,6 +3595,9 @@ + + + From 4ea12f851e9244388e2315a330a2849cb5fff145 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Thu, 19 Mar 2026 20:36:56 +0200 Subject: [PATCH 04/15] Add test data --- .../android/messaging/debug/TestDataSeeder.kt | 1054 +++++++++++++++++ .../messaging/util/BugleGservicesKeys.java | 4 +- .../android/messaging/util/DebugUtils.java | 26 + .../util/db/ext/DatabaseWrapperExtensions.kt | 13 + 4 files changed, 1096 insertions(+), 1 deletion(-) create mode 100644 src/com/android/messaging/debug/TestDataSeeder.kt create mode 100644 src/com/android/messaging/util/db/ext/DatabaseWrapperExtensions.kt diff --git a/src/com/android/messaging/debug/TestDataSeeder.kt b/src/com/android/messaging/debug/TestDataSeeder.kt new file mode 100644 index 00000000..b5f3de11 --- /dev/null +++ b/src/com/android/messaging/debug/TestDataSeeder.kt @@ -0,0 +1,1054 @@ +@file:JvmName("TestDataSeeder") + +package com.android.messaging.debug + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.net.Uri +import androidx.core.graphics.createBitmap +import com.android.messaging.datamodel.DataModel +import com.android.messaging.datamodel.DatabaseHelper +import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns +import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns +import com.android.messaging.datamodel.DatabaseHelper.MessageColumns +import com.android.messaging.datamodel.DatabaseHelper.PartColumns +import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns +import com.android.messaging.datamodel.DatabaseWrapper +import com.android.messaging.datamodel.MessagingContentProvider +import com.android.messaging.datamodel.data.MessageData +import com.android.messaging.datamodel.data.ParticipantData +import com.android.messaging.util.ContentType +import com.android.messaging.util.LogUtil +import com.android.messaging.util.db.ext.withTransaction +import java.io.File +import java.io.FileOutputStream + +private const val TAG = "TestDataSeeder" +private const val TEST_PHONE_PREFIX = "+15550" + +private const val MINUTES = 60 * 1000L +private const val HOURS = 60 * MINUTES +private const val DAYS = 24 * HOURS + +fun seedTestData(context: Context) { + val db = DataModel.get().getDatabase() + + val selfId = findSelfParticipantId(db) ?: run { + LogUtil.w(TAG, "No self participant found — open the app at least once before seeding") + return + } + + val testImages = buildTestImages(context) + val now = System.currentTimeMillis() + + db.withTransaction { + val alice = upsertParticipant(db, "${TEST_PHONE_PREFIX}001234", "Alice Wonderland", "Alice") + val bob = upsertParticipant(db, "${TEST_PHONE_PREFIX}005678", "Bob Baker", "Bob") + val carol = upsertParticipant(db, "${TEST_PHONE_PREFIX}002345", "Carol Chen", "Carol") + val dave = upsertParticipant(db, "${TEST_PHONE_PREFIX}003456", "Dave Diaz", "Dave") + val eve = upsertParticipant(db, "${TEST_PHONE_PREFIX}004567", "Eve Evans", "Eve") + val frank = upsertParticipant(db, "${TEST_PHONE_PREFIX}006789", "Frank Ford", "Frank") + val grace = upsertParticipant(db, "${TEST_PHONE_PREFIX}007890", "Grace Green", "Grace") + val henry = upsertParticipant(db, "${TEST_PHONE_PREFIX}008901", "Henry Hall", "Henry") + val iris = upsertParticipant(db, "${TEST_PHONE_PREFIX}009012", "Iris Ingram", "Iris") + val jack = upsertParticipant(db, "${TEST_PHONE_PREFIX}010123", "Jack Johnson", "Jack") + + seedScenarioA(db, selfId, alice, now) + seedScenarioB(db, selfId, bob, now) + seedScenarioC(db, selfId, carol, dave, eve, now) + seedScenarioD(db, selfId, frank, now) + seedScenarioE(db, selfId, grace, now) + seedScenarioF(db, selfId, henry, now) + seedScenarioG(db, selfId, iris, testImages, now) + seedScenarioH(db, selfId, jack, carol, testImages, now) + seedScenarioI(db, selfId, carol, dave, eve, now) + } + + MessagingContentProvider.notifyConversationListChanged() + LogUtil.d(TAG, "Test data seeded successfully") +} + +fun clearSeededTestData(context: Context) { + val db = DataModel.get().getDatabase() + + db.withTransaction { + val participantIds = mutableListOf() + db.query( + DatabaseHelper.PARTICIPANTS_TABLE, + arrayOf(ParticipantColumns._ID), + "${ParticipantColumns.NORMALIZED_DESTINATION} LIKE ?", + arrayOf("$TEST_PHONE_PREFIX%"), + null, + null, + null + )?.use { cursor -> + while (cursor.moveToNext()) participantIds.add(cursor.getString(0)) + } + + if (participantIds.isEmpty()) { + db.setTransactionSuccessful() + return + } + + val pPlaceholders = participantIds.joinToString(",") { "?" } + val pArgs = participantIds.toTypedArray() + + val conversationIds = mutableListOf() + db.query( + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE, + arrayOf(ConversationParticipantsColumns.CONVERSATION_ID), + "${ConversationParticipantsColumns.PARTICIPANT_ID} IN ($pPlaceholders)", + pArgs, + null, + null, + null + )?.use { cursor -> + while (cursor.moveToNext()) conversationIds.add(cursor.getString(0)) + } + + if (conversationIds.isNotEmpty()) { + // ON DELETE CASCADE handles messages and parts + db.delete( + DatabaseHelper.CONVERSATIONS_TABLE, + "${ConversationColumns._ID} IN (${conversationIds.joinToString(",") { "?" }})", + conversationIds.toTypedArray() + ) + } + + db.delete( + DatabaseHelper.PARTICIPANTS_TABLE, + "${ParticipantColumns._ID} IN ($pPlaceholders)", + pArgs + ) + } + + // Also clean up the image files from cache + for (i in 1..3) { + File(context.cacheDir, "seed_img_$i.jpg").delete() + } + + MessagingContentProvider.notifyConversationListChanged() + LogUtil.d(TAG, "Seeded test data cleared") +} + +private fun buildTestImages(context: Context): List { + val specs = listOf( + Triple("seed_img_1.jpg", Color.rgb(100, 149, 237), "Photo 1"), + Triple("seed_img_2.jpg", Color.rgb(144, 238, 144), "Photo 2"), + Triple("seed_img_3.jpg", Color.rgb(255, 160, 122), "Photo 3") + ) + + return specs.map { (filename, bgColor, label) -> + val file = File(context.cacheDir, filename) + if (!file.exists()) { + val bmp = createBitmap(400, 300) + val canvas = Canvas(bmp) + canvas.drawColor(bgColor) + val paint = Paint().apply { + color = Color.WHITE + textSize = 48f + isAntiAlias = true + isFakeBoldText = true + } + canvas.drawText(label, 130f, 165f, paint) + FileOutputStream(file).use { bmp.compress(Bitmap.CompressFormat.JPEG, 90, it) } + bmp.recycle() + } + Uri.fromFile(file).toString() + } +} + +private fun findSelfParticipantId(db: DatabaseWrapper): String? = db.query( + DatabaseHelper.PARTICIPANTS_TABLE, + arrayOf(ParticipantColumns._ID), + "${ParticipantColumns.SUB_ID} != ?", + arrayOf(ParticipantData.OTHER_THAN_SELF_SUB_ID.toString()), + null, + null, + "${ParticipantColumns._ID} ASC", + "1" +)?.use { cursor -> + if (cursor.moveToFirst()) cursor.getString(0) else null +} + +private fun upsertParticipant( + db: DatabaseWrapper, + phone: String, + fullName: String, + firstName: String, +): String { + db.insertWithOnConflict( + DatabaseHelper.PARTICIPANTS_TABLE, + null, + ContentValues().apply { + put(ParticipantColumns.SUB_ID, ParticipantData.OTHER_THAN_SELF_SUB_ID) + put(ParticipantColumns.NORMALIZED_DESTINATION, phone) + put(ParticipantColumns.SEND_DESTINATION, phone) + put(ParticipantColumns.DISPLAY_DESTINATION, phone) + put(ParticipantColumns.FULL_NAME, fullName) + put(ParticipantColumns.FIRST_NAME, firstName) + }, + SQLiteDatabase.CONFLICT_IGNORE + ) + + return db + .query( + DatabaseHelper.PARTICIPANTS_TABLE, + arrayOf(ParticipantColumns._ID), + "${ParticipantColumns.NORMALIZED_DESTINATION} = ? AND ${ParticipantColumns.SUB_ID} = ?", + arrayOf(phone, ParticipantData.OTHER_THAN_SELF_SUB_ID.toString()), + null, + null, + null + ) + ?.use { cursor -> + cursor.moveToFirst() + cursor.getString(0) + } + .let(::requireNotNull) +} + +private fun createConversation( + db: DatabaseWrapper, + name: String, + selfId: String, + participantIds: List, + sortTimestamp: Long, + previewUri: String? = null, + previewContentType: String? = null, +): Long { + val conversationId = db.insert( + DatabaseHelper.CONVERSATIONS_TABLE, + null, + ContentValues().apply { + put(ConversationColumns.NAME, name) + put(ConversationColumns.CURRENT_SELF_ID, selfId) + put(ConversationColumns.PARTICIPANT_COUNT, participantIds.size) + put(ConversationColumns.SORT_TIMESTAMP, sortTimestamp) + put(ConversationColumns.ARCHIVE_STATUS, 0) + put(ConversationColumns.NOTIFICATION_ENABLED, 1) + put(ConversationColumns.NOTIFICATION_VIBRATION, 1) + if (previewUri != null) put(ConversationColumns.PREVIEW_URI, previewUri) + if (previewContentType != + null + ) { + put(ConversationColumns.PREVIEW_CONTENT_TYPE, previewContentType) + } + } + ) + for (participantId in participantIds) { + db.insert( + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE, + null, + ContentValues().apply { + put(ConversationParticipantsColumns.CONVERSATION_ID, conversationId) + put(ConversationParticipantsColumns.PARTICIPANT_ID, participantId) + } + ) + } + return conversationId +} + +private fun insertTextMessage( + db: DatabaseWrapper, + conversationId: Long, + senderId: String, + selfId: String, + text: String, + status: Int, + protocol: Int, + timestamp: Long, + seen: Boolean = true, + read: Boolean = true, + mmsSubject: String? = null, +): Long { + val messageId = insertMessageRow( + db, conversationId, senderId, selfId, + status, protocol, timestamp, seen, read, mmsSubject + ) + db.insert( + DatabaseHelper.PARTS_TABLE, + null, + ContentValues().apply { + put(PartColumns.MESSAGE_ID, messageId) + put(PartColumns.CONVERSATION_ID, conversationId) + put(PartColumns.TEXT, text) + put(PartColumns.CONTENT_TYPE, ContentType.TEXT_PLAIN) + } + ) + return messageId +} + +private fun insertImageMessage( + db: DatabaseWrapper, + conversationId: Long, + senderId: String, + selfId: String, + imageUri: String, + status: Int, + timestamp: Long, + seen: Boolean = true, + read: Boolean = true, +): Long { + val messageId = insertMessageRow( + db, conversationId, senderId, selfId, + status, MessageData.PROTOCOL_MMS, timestamp, seen, read, mmsSubject = null + ) + db.insert( + DatabaseHelper.PARTS_TABLE, + null, + ContentValues().apply { + put(PartColumns.MESSAGE_ID, messageId) + put(PartColumns.CONVERSATION_ID, conversationId) + put(PartColumns.CONTENT_TYPE, ContentType.IMAGE_JPEG) + put(PartColumns.CONTENT_URI, imageUri) + put(PartColumns.WIDTH, 400) + put(PartColumns.HEIGHT, 300) + } + ) + return messageId +} + +private fun insertMixedMessage( + db: DatabaseWrapper, + conversationId: Long, + senderId: String, + selfId: String, + text: String, + imageUri: String, + status: Int, + timestamp: Long, + seen: Boolean = true, + read: Boolean = true, +): Long { + val messageId = insertMessageRow( + db, conversationId, senderId, selfId, + status, MessageData.PROTOCOL_MMS, timestamp, seen, read, mmsSubject = null + ) + db.insert( + DatabaseHelper.PARTS_TABLE, + null, + ContentValues().apply { + put(PartColumns.MESSAGE_ID, messageId) + put(PartColumns.CONVERSATION_ID, conversationId) + put(PartColumns.CONTENT_TYPE, ContentType.IMAGE_JPEG) + put(PartColumns.CONTENT_URI, imageUri) + put(PartColumns.WIDTH, 400) + put(PartColumns.HEIGHT, 300) + } + ) + db.insert( + DatabaseHelper.PARTS_TABLE, + null, + ContentValues().apply { + put(PartColumns.MESSAGE_ID, messageId) + put(PartColumns.CONVERSATION_ID, conversationId) + put(PartColumns.TEXT, text) + put(PartColumns.CONTENT_TYPE, ContentType.TEXT_PLAIN) + } + ) + return messageId +} + +private fun insertMessageRow( + db: DatabaseWrapper, + conversationId: Long, + senderId: String, + selfId: String, + status: Int, + protocol: Int, + timestamp: Long, + seen: Boolean, + read: Boolean, + mmsSubject: String?, +): Long = db.insert( + DatabaseHelper.MESSAGES_TABLE, + null, + ContentValues().apply { + put(MessageColumns.CONVERSATION_ID, conversationId) + put(MessageColumns.SENDER_PARTICIPANT_ID, senderId) + put(MessageColumns.SELF_PARTICIPANT_ID, selfId) + put(MessageColumns.STATUS, status) + put(MessageColumns.PROTOCOL, protocol) + put(MessageColumns.SENT_TIMESTAMP, timestamp) + put(MessageColumns.RECEIVED_TIMESTAMP, timestamp) + put(MessageColumns.SEEN, if (seen) 1 else 0) + put(MessageColumns.READ, if (read) 1 else 0) + if (mmsSubject != null) put(MessageColumns.MMS_SUBJECT, mmsSubject) + } +) + +private fun finalizeConversation( + db: DatabaseWrapper, + conversationId: Long, + latestMessageId: Long, + latestTimestamp: Long, + snippetText: String, + previewUri: String? = null, + previewContentType: String? = null, +) { + db.update( + DatabaseHelper.CONVERSATIONS_TABLE, + ContentValues().apply { + put(ConversationColumns.LATEST_MESSAGE_ID, latestMessageId) + put(ConversationColumns.SORT_TIMESTAMP, latestTimestamp) + put(ConversationColumns.SNIPPET_TEXT, snippetText) + if (previewUri != null) put(ConversationColumns.PREVIEW_URI, previewUri) + if (previewContentType != + null + ) { + put(ConversationColumns.PREVIEW_CONTENT_TYPE, previewContentType) + } + }, + "${ConversationColumns._ID} = ?", + arrayOf(conversationId.toString()) + ) +} + +/** + * 1:1 SMS thread with Alice, 40 messages. + */ +private fun seedScenarioA(db: DatabaseWrapper, selfId: String, aliceId: String, now: Long) { + val baseTime = now - 4 * DAYS + val convId = createConversation(db, "Alice Wonderland", selfId, listOf(aliceId), baseTime) + + val texts = listOf( + "Hey, are you free this weekend?", + "I was thinking we could grab coffee", + "There's a new place downtown that opened last week", + "Yeah sounds good! What time works for you?", + "How about 10am Saturday?", + "Perfect, see you then", + "Can you also bring the book you mentioned?", + "Sure, I'll bring it", + "Did you see the game last night?", + "No I missed it, who won?", + "It went to overtime, crazy finish", + "I'll have to watch the highlights", + "Just got back from the gym", + "Nice, how was it?", + "Pretty good, trying the new routine", + "Let me know how it goes", + "Will do", + "See you Saturday!", + "Looking forward to it", + "Don't forget to bring the book" + ) + + var latestMsgId = 0L + var latestTime = baseTime + for (i in 0 until 40) { + // 3-message clusters 2 min apart; 38 min gap between clusters + val clusterIndex = i / 3 + val withinCluster = i % 3 + val msgTime = baseTime + clusterIndex * 38 * MINUTES + withinCluster * 2 * MINUTES + val isIncoming = withinCluster != 1 // pattern: in, out, in, in, out, in, ... + val senderId = if (isIncoming) aliceId else selfId + val status = if (isIncoming) { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + } else { + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + } + latestMsgId = insertTextMessage( + db, + convId, + senderId, + selfId, + texts[i % texts.size], + status, + MessageData.PROTOCOL_SMS, + msgTime + ) + latestTime = msgTime + } + + finalizeConversation(db, convId, latestMsgId, latestTime, "Don't forget to bring the book") +} + +/** + * 1:1 SMS thread with Bob containing a failed message and one retrying. + */ +private fun seedScenarioB(db: DatabaseWrapper, selfId: String, bobId: String, now: Long) { + val baseTime = now - 2 * DAYS + val convId = createConversation(db, "Bob Baker", selfId, listOf(bobId), baseTime) + + // (text, isIncoming, status) + val messages = listOf( + Triple("Did you get the report?", false, MessageData.BUGLE_STATUS_OUTGOING_COMPLETE), + Triple("Yeah got it, looks good", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple( + "Can you send the updated version?", + false, + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ), + Triple("Sure, give me a minute", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple("Here you go", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple("Thanks! One more thing...", false, MessageData.BUGLE_STATUS_OUTGOING_COMPLETE), + Triple("What is it?", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple("Can we meet tomorrow at 3pm?", false, MessageData.BUGLE_STATUS_OUTGOING_FAILED), + Triple("OK let me check", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple("3pm works for me", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple("Can we meet tomorrow at 3pm?", false, MessageData.BUGLE_STATUS_OUTGOING_COMPLETE), + Triple("Great, see you then", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple("Perfect", false, MessageData.BUGLE_STATUS_OUTGOING_COMPLETE), + Triple("Don't be late!", true, MessageData.BUGLE_STATUS_INCOMING_COMPLETE), + Triple("Never", false, MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY) + ) + + var latestMsgId = 0L + var latestTime = baseTime + for ((idx, m) in messages.withIndex()) { + val (text, isIncoming, status) = m + val msgTime = baseTime + idx * 8 * MINUTES + val senderId = if (isIncoming) bobId else selfId + latestMsgId = insertTextMessage( + db, + convId, + senderId, + selfId, + text, + status, + MessageData.PROTOCOL_SMS, + msgTime + ) + latestTime = msgTime + } + + finalizeConversation(db, convId, latestMsgId, latestTime, "Never") +} + +/** + * Group MMS thread "Team Chat" with Carol, Dave, Eve (30 messages) + */ +private fun seedScenarioC( + db: DatabaseWrapper, + selfId: String, + carolId: String, + daveId: String, + eveId: String, + now: Long, +) { + val baseTime = now - 3 * DAYS + val convId = createConversation( + db, + "Team Chat", + selfId, + listOf(carolId, daveId, eveId), + baseTime + ) + + val senders = listOf(carolId, daveId, eveId, selfId) + val texts = listOf( + "Hey everyone!", "Hi there", "Hello!", "What time are we meeting?", + "How about 2pm?", "Works for me", "Can't do 2pm, how about 3?", "3pm is fine", + "Let's do 3pm then", "Should we bring anything?", "Just yourselves", "I'll bring snacks", + "Nice!", "See you all tomorrow", "Can't wait", "It's going to be great", + "Agreed", "Anyone need a ride?", "I'm good, thanks", "I could use one actually", + "I got you Carol", "Thanks Dave!", "Ok see everyone at 3", "See you there!", + "Don't forget it's at the usual place", "Got it", "See you all soon!", + "This is going to be fun", "Definitely", "On my way!" + ) + + var latestMsgId = 0L + var latestTime = baseTime + for (i in texts.indices) { + val sender = senders[i % senders.size] + val status = if (sender == selfId) { + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + } else { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + } + val msgTime = baseTime + i * 5 * MINUTES + latestMsgId = insertTextMessage( + db, + convId, + sender, + selfId, + texts[i], + status, + MessageData.PROTOCOL_MMS, + msgTime + ) + latestTime = msgTime + } + + finalizeConversation(db, convId, latestMsgId, latestTime, "On my way!") +} + +/** + * MMS thread with Frank where every message has a subject line + */ +private fun seedScenarioD(db: DatabaseWrapper, selfId: String, frankId: String, now: Long) { + val baseTime = now - 5 * DAYS + val convId = createConversation(db, "Frank Ford", selfId, listOf(frankId), baseTime) + + val messages = listOf( + Pair("Are you still up for hiking Saturday?", false), + Pair("Yes! Super excited", true), + Pair("Great, let's meet at the trailhead at 8am", false), + Pair("Works for me. Which trail?", true), + Pair("The ridge trail, it has the best views", false), + Pair("Oh I love that trail!", true), + Pair("Bring sunscreen, it'll be hot", false), + Pair("Good call", true), + Pair("See you Saturday!", false), + Pair("Can't wait!", true) + ) + + var latestMsgId = 0L + var latestTime = baseTime + for ((idx, m) in messages.withIndex()) { + val (text, isIncoming) = m + val msgTime = baseTime + idx * 10 * MINUTES + val senderId = if (isIncoming) frankId else selfId + val status = if (isIncoming) { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + } else { + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + } + latestMsgId = insertTextMessage( + db, convId, senderId, selfId, + text, status, MessageData.PROTOCOL_MMS, msgTime, mmsSubject = "Weekend plans" + ) + latestTime = msgTime + } + + finalizeConversation(db, convId, latestMsgId, latestTime, "Can't wait!") +} + +/** + * Long thread with Grace, 300 messages, last 5 unread + */ +private fun seedScenarioE(db: DatabaseWrapper, selfId: String, graceId: String, now: Long) { + val totalMessages = 300 + val baseTime = now - 10 * HOURS + val unreadStartIndex = totalMessages - 5 + val convId = createConversation(db, "Grace Green", selfId, listOf(graceId), baseTime) + + var latestMsgId = 0L + var latestText = "" + for (i in 0 until totalMessages) { + val msgTime = baseTime + i * 2 * MINUTES + val isIncoming = i % 2 == 0 + val senderId = if (isIncoming) graceId else selfId + val status = if (isIncoming) { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + } else { + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + } + val isUnread = i >= unreadStartIndex + latestText = "Message ${i + 1} — scroll performance test" + latestMsgId = insertTextMessage( + db, convId, senderId, selfId, + latestText, status, MessageData.PROTOCOL_SMS, msgTime, + seen = !isUnread, read = !isUnread + ) + } + + finalizeConversation( + db, + convId, + latestMsgId, + baseTime + totalMessages * 2 * MINUTES, + latestText + ) +} + +/** + * All-unread conversation with Henry, 5 incoming messages + */ +private fun seedScenarioF(db: DatabaseWrapper, selfId: String, henryId: String, now: Long) { + val baseTime = now - 30 * MINUTES + val convId = createConversation(db, "Henry Hall", selfId, listOf(henryId), baseTime) + + val texts = listOf( + "Hey, are you around?", + "I need to show you something", + "It's important", + "Please reply when you get a chance", + "I'll be online for the next hour" + ) + + var latestMsgId = 0L + var latestTime = baseTime + for ((idx, text) in texts.withIndex()) { + val msgTime = baseTime + idx * 5 * MINUTES + latestMsgId = insertTextMessage( + db, convId, henryId, selfId, text, + MessageData.BUGLE_STATUS_INCOMING_COMPLETE, MessageData.PROTOCOL_SMS, msgTime, + seen = false, read = false + ) + latestTime = msgTime + } + + finalizeConversation(db, convId, latestMsgId, latestTime, texts.last()) +} + +/** + * 1:1 MMS thread with Iris containing image-only, text+image, and text-only messages + */ +private fun seedScenarioG( + db: DatabaseWrapper, + selfId: String, + irisId: String, + images: List, + now: Long, +) { + val img1 = images[0] + val img2 = images[1] + val img3 = images[2] + val baseTime = now - 1 * DAYS + + val convId = createConversation( + db, + "Iris Ingram", + selfId, + listOf(irisId), + baseTime, + previewUri = img1, + previewContentType = ContentType.IMAGE_JPEG + ) + + data class Msg( + val type: String, + val text: String = "", + val imageUri: String = "", + val isIncoming: Boolean, + ) + + val messages = listOf( + Msg("text", text = "Hey! Check out what I found", isIncoming = true), + Msg("image", imageUri = img1, isIncoming = true), + Msg("text", text = "Wow that looks amazing!", isIncoming = false), + Msg("text", text = "Where was this taken?", isIncoming = false), + Msg("text", text = "At the botanical garden last weekend", isIncoming = true), + Msg( + "mixed", + text = "Here's another one from the same day", + imageUri = img2, + isIncoming = true + ), + Msg("text", text = "These are stunning", isIncoming = false), + Msg("image", imageUri = img3, isIncoming = false), + Msg("text", text = "I took that one on the way home", isIncoming = false), + Msg("text", text = "You have such a good eye for photos!", isIncoming = true), + Msg("text", text = "Thanks! We should go together sometime", isIncoming = false), + Msg("text", text = "Definitely, let me know when you're free", isIncoming = true), + Msg("image", imageUri = img2, isIncoming = true), + Msg("text", text = "And one more from yesterday", isIncoming = true), + Msg( + "mixed", + text = "Shot this from my window this morning", + imageUri = img1, + isIncoming = false + ) + ) + + var latestMsgId = 0L + var latestTime = baseTime + for ((idx, m) in messages.withIndex()) { + val msgTime = baseTime + idx * 12 * MINUTES + val senderId = if (m.isIncoming) irisId else selfId + val status = if (m.isIncoming) { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + } else { + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + } + latestMsgId = when (m.type) { + "image" -> insertImageMessage( + db, + convId, + senderId, + selfId, + m.imageUri, + status, + msgTime + ) + + "mixed" -> insertMixedMessage( + db, + convId, + senderId, + selfId, + m.text, + m.imageUri, + status, + msgTime + ) + + else -> insertTextMessage( + db, + convId, + senderId, + selfId, + m.text, + status, + MessageData.PROTOCOL_MMS, + msgTime + ) + } + latestTime = msgTime + } + + finalizeConversation( + db, + convId, + latestMsgId, + latestTime, + "Shot this from my window this morning", + previewUri = img1, + previewContentType = ContentType.IMAGE_JPEG + ) +} + +/** + * Group MMS thread "Photo Dump" with Jack and Carol containing image bursts + */ +private fun seedScenarioH( + db: DatabaseWrapper, + selfId: String, + jackId: String, + carolId: String, + images: List, + now: Long, +) { + val img1 = images[0] + val img2 = images[1] + val img3 = images[2] + val baseTime = now - 6 * HOURS + + val convId = createConversation( + db, + "Photo Dump", + selfId, + listOf(jackId, carolId), + baseTime, + previewUri = img2, + previewContentType = ContentType.IMAGE_JPEG + ) + + data class Msg( + val type: String, + val text: String = "", + val imageUri: String = "", + val senderId: String, + ) + + val messages = listOf( + Msg("text", text = "Dropping some pics from last night", senderId = jackId), + Msg("image", imageUri = img1, senderId = jackId), + Msg("image", imageUri = img3, senderId = jackId), + Msg("text", text = "The lighting was perfect", senderId = jackId), + Msg("text", text = "These are great Jack!", senderId = carolId), + Msg("image", imageUri = img2, senderId = carolId), + Msg("text", text = "I got a few too", senderId = carolId), + Msg("text", text = "Love that shot Carol", senderId = selfId), + Msg("mixed", text = "Here's mine from the same spot", imageUri = img3, senderId = selfId), + Msg("text", text = "We all had the same idea haha", senderId = jackId), + Msg("image", imageUri = img1, senderId = carolId), + Msg("text", text = "One more", senderId = carolId), + Msg("text", text = "We need to do this again soon", senderId = selfId), + Msg("text", text = "+1", senderId = jackId), + Msg("text", text = "Same time next week?", senderId = carolId) + ) + + var latestMsgId = 0L + var latestTime = baseTime + for ((idx, m) in messages.withIndex()) { + val msgTime = baseTime + idx * 7 * MINUTES + val status = if (m.senderId == selfId) { + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + } else { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + } + latestMsgId = when (m.type) { + "image" -> insertImageMessage( + db, + convId, + m.senderId, + selfId, + m.imageUri, + status, + msgTime + ) + + "mixed" -> insertMixedMessage( + db, + convId, + m.senderId, + selfId, + m.text, + m.imageUri, + status, + msgTime + ) + + else -> insertTextMessage( + db, + convId, + m.senderId, + selfId, + m.text, + status, + MessageData.PROTOCOL_MMS, + msgTime + ) + } + latestTime = msgTime + } + + finalizeConversation( + db, + convId, + latestMsgId, + latestTime, + "Same time next week?", + previewUri = img2, + previewContentType = ContentType.IMAGE_JPEG + ) +} + +/** + * Group MMS thread containing explicit clustering and non-clustering cases + */ +private fun seedScenarioI( + db: DatabaseWrapper, + selfId: String, + carolId: String, + daveId: String, + eveId: String, + now: Long, +) { + val baseTime = now - 20 * MINUTES + val conversationId = createConversation( + db = db, + name = "Clustering Test Cases", + selfId = selfId, + participantIds = listOf(carolId, daveId, eveId), + sortTimestamp = baseTime + ) + + data class ClusterTestMessage(val text: String, val senderId: String, val offsetMillis: Long) + + val messages = listOf( + ClusterTestMessage( + text = "Standalone incoming", + senderId = carolId, + offsetMillis = 0L + ), + ClusterTestMessage( + text = "Pair top", + senderId = carolId, + offsetMillis = 2 * MINUTES + ), + ClusterTestMessage( + text = "Pair bottom", + senderId = carolId, + offsetMillis = 2 * MINUTES + 30_000L + ), + ClusterTestMessage( + text = "Triplet top", + senderId = daveId, + offsetMillis = 5 * MINUTES + ), + ClusterTestMessage( + text = "Triplet middle", + senderId = daveId, + offsetMillis = 5 * MINUTES + 20_000L + ), + ClusterTestMessage( + text = "Triplet bottom", + senderId = daveId, + offsetMillis = 5 * MINUTES + 40_000L + ), + ClusterTestMessage( + text = "Quartet top", + senderId = eveId, + offsetMillis = 8 * MINUTES + ), + ClusterTestMessage( + text = "Quartet middle 1", + senderId = eveId, + offsetMillis = 8 * MINUTES + 20_000L + ), + ClusterTestMessage( + text = "Quartet middle 2", + senderId = eveId, + offsetMillis = 8 * MINUTES + 40_000L + ), + ClusterTestMessage( + text = "Quartet bottom", + senderId = eveId, + offsetMillis = 9 * MINUTES + ), + ClusterTestMessage( + text = "Same sender after gap", + senderId = daveId, + offsetMillis = 12 * MINUTES + ), + ClusterTestMessage( + text = "Gap break still standalone", + senderId = daveId, + offsetMillis = 13 * MINUTES + 40_000L + ), + ClusterTestMessage( + text = "Different sender break", + senderId = carolId, + offsetMillis = 16 * MINUTES + ), + ClusterTestMessage( + text = "Outgoing standalone", + senderId = selfId, + offsetMillis = 16 * MINUTES + 20_000L + ), + ClusterTestMessage( + text = "Outgoing pair top", + senderId = selfId, + offsetMillis = 19 * MINUTES + ), + ClusterTestMessage( + text = "Outgoing pair bottom", + senderId = selfId, + offsetMillis = 19 * MINUTES + 20_000L + ) + ) + + var latestMessageId = 0L + var latestTimestamp = baseTime + var latestText = "" + for (message in messages) { + val timestamp = baseTime + message.offsetMillis + val isIncoming = message.senderId != selfId + val status = if (isIncoming) { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + } else { + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + } + + latestText = message.text + latestMessageId = insertTextMessage( + db = db, + conversationId = conversationId, + senderId = message.senderId, + selfId = selfId, + text = message.text, + status = status, + protocol = MessageData.PROTOCOL_MMS, + timestamp = timestamp + ) + latestTimestamp = timestamp + } + + finalizeConversation( + db = db, + conversationId = conversationId, + latestMessageId = latestMessageId, + latestTimestamp = latestTimestamp, + snippetText = latestText + ) +} diff --git a/src/com/android/messaging/util/BugleGservicesKeys.java b/src/com/android/messaging/util/BugleGservicesKeys.java index 678fb98a..242ccaf8 100644 --- a/src/com/android/messaging/util/BugleGservicesKeys.java +++ b/src/com/android/messaging/util/BugleGservicesKeys.java @@ -17,6 +17,8 @@ package com.android.messaging.util; +import com.android.messaging.BuildConfig; + /** * List of gservices keys and default values which are in use. */ @@ -30,7 +32,7 @@ private BugleGservicesKeys() {} // do not instantiate public static final String ENABLE_DEBUGGING_FEATURES = "bugle_debugging"; public static final boolean ENABLE_DEBUGGING_FEATURES_DEFAULT - = false; + = BuildConfig.DEBUG; /** * Whether to enable saving extra logs. Default is {@value #ENABLE_LOG_SAVER_DEFAULT}. diff --git a/src/com/android/messaging/util/DebugUtils.java b/src/com/android/messaging/util/DebugUtils.java index 9027fae5..7212c42c 100644 --- a/src/com/android/messaging/util/DebugUtils.java +++ b/src/com/android/messaging/util/DebugUtils.java @@ -27,6 +27,7 @@ import android.telephony.SmsMessage; import android.text.TextUtils; import android.widget.ArrayAdapter; +import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -38,6 +39,7 @@ import com.android.messaging.datamodel.SyncManager; import com.android.messaging.datamodel.action.DumpDatabaseAction; import com.android.messaging.datamodel.action.LogTelephonyDatabaseAction; +import com.android.messaging.debug.TestDataSeeder; import com.android.messaging.sms.MmsUtils; import com.android.messaging.ui.UIIntents; import com.android.messaging.ui.debug.DebugSmsMmsFromDumpFileDialogFragment; @@ -191,6 +193,30 @@ public void run() { } }); + arrayAdapter.add(new DebugAction("Seed test data") { + @Override + public void run() { + SafeAsyncTask.executeOnThreadPool(() -> { + TestDataSeeder.seedTestData(host); + ThreadUtil.getMainThreadHandler().post(() -> + Toast.makeText(host, "Test data seeded", Toast.LENGTH_SHORT).show() + ); + }); + } + }); + + arrayAdapter.add(new DebugAction("Clear seeded test data") { + @Override + public void run() { + SafeAsyncTask.executeOnThreadPool(() -> { + TestDataSeeder.clearSeededTestData(host); + ThreadUtil.getMainThreadHandler().post(() -> + Toast.makeText(host, "Seeded test data cleared", Toast.LENGTH_SHORT).show() + ); + }); + } + }); + builder.setAdapter(arrayAdapter, new android.content.DialogInterface.OnClickListener() { @Override diff --git a/src/com/android/messaging/util/db/ext/DatabaseWrapperExtensions.kt b/src/com/android/messaging/util/db/ext/DatabaseWrapperExtensions.kt new file mode 100644 index 00000000..89dbf0ed --- /dev/null +++ b/src/com/android/messaging/util/db/ext/DatabaseWrapperExtensions.kt @@ -0,0 +1,13 @@ +package com.android.messaging.util.db.ext + +import com.android.messaging.datamodel.DatabaseWrapper + +inline fun DatabaseWrapper.withTransaction(block: () -> Unit) { + beginTransaction() + try { + block() + setTransactionSuccessful() + } finally { + endTransaction() + } +} From 21d0272f3f01053a8a377ce01fa46fb464c20a9e Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Thu, 19 Mar 2026 20:53:52 +0200 Subject: [PATCH 05/15] Add Compose dependencies --- app/build.gradle.kts | 34 +- build.gradle.kts | 1 + gradle/verification-metadata.xml | 1547 ++++++++++++++++++++++++++++++ settings.gradle.kts | 1 + 4 files changed, 1581 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84d0ecdb..dc38985a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ import java.util.Properties plugins { id("com.android.application") + id("org.jetbrains.kotlin.plugin.compose") } java { @@ -39,6 +40,7 @@ android { buildFeatures { buildConfig = true resValues = true + compose = true } sourceSets.getByName("main") { @@ -92,15 +94,43 @@ android { dependencies { implementation("androidx.appcompat:appcompat:1.7.1") - implementation("androidx.preference:preference:1.2.1") implementation("androidx.palette:palette:1.0.0") + implementation("androidx.preference:preference:1.2.1") implementation("androidx.recyclerview:recyclerview:1.4.0") + + val composeBom = platform("androidx.compose:compose-bom:2026.03.00") + implementation(composeBom) + + implementation("androidx.activity:activity-compose:1.13.0") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.foundation:foundation-layout") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.10.0") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0") + + implementation("io.coil-kt.coil3:coil-compose:3.4.0") implementation("com.github.bumptech.glide:glide:5.0.5") + implementation("com.google.guava:guava:33.5.0-android") - implementation("com.googlecode.libphonenumber:libphonenumber:9.0.26") implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") + + implementation("com.googlecode.libphonenumber:libphonenumber:9.0.26") + implementation(project(":lib:platform_frameworks_opt_chips")) implementation(project(":lib:platform_frameworks_opt_photoviewer")) implementation(project(":lib:platform_frameworks_opt_vcard")) + + debugImplementation("androidx.compose.ui:ui-test-manifest") + debugImplementation("androidx.compose.ui:ui-tooling") + + androidTestImplementation(composeBom) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") } diff --git a/build.gradle.kts b/build.gradle.kts index 632c4f0b..25cc7e78 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("com.android.application") version "9.1.0" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false } buildscript { diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d2dde522..dc5f669c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2551,6 +2551,9 @@ + + + @@ -2949,6 +2952,12 @@ + + + + + + @@ -3676,6 +3685,1544 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index fe6f2489..3d8f30d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ pluginManagement { repositories { google() mavenCentral() + gradlePluginPortal() } } dependencyResolutionManagement { From c2e4ec8782ee070ce0aa4cc48ac5c9f62092dc87 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Thu, 19 Mar 2026 21:21:20 +0200 Subject: [PATCH 06/15] Migrate to Gradle version catalog --- app/build.gradle.kts | 58 ++++++++++++++++++------------------ build.gradle.kts | 8 ++--- gradle/libs.versions.toml | 62 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dc38985a..8180ba7f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,8 +2,8 @@ import java.io.FileInputStream import java.util.Properties plugins { - id("com.android.application") - id("org.jetbrains.kotlin.plugin.compose") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) } java { @@ -93,44 +93,42 @@ android { } dependencies { - implementation("androidx.appcompat:appcompat:1.7.1") - implementation("androidx.palette:palette:1.0.0") - implementation("androidx.preference:preference:1.2.1") - implementation("androidx.recyclerview:recyclerview:1.4.0") + implementation(libs.androidx.appcompat) + implementation(libs.androidx.palette) + implementation(libs.androidx.preference) + implementation(libs.androidx.recyclerview) - val composeBom = platform("androidx.compose:compose-bom:2026.03.00") - implementation(composeBom) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) - implementation("androidx.activity:activity-compose:1.13.0") - implementation("androidx.compose.foundation:foundation") - implementation("androidx.compose.foundation:foundation-layout") - implementation("androidx.compose.material:material-icons-extended") - implementation("androidx.compose.material3:material3") - implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-tooling-preview") + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.10.0") - implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0") + implementation(libs.coil.compose) + implementation(libs.glide) - implementation("io.coil-kt.coil3:coil-compose:3.4.0") - implementation("com.github.bumptech.glide:glide:5.0.5") + implementation(libs.guava) + implementation(libs.jsr305) - implementation("com.google.guava:guava:33.5.0-android") - implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation(libs.kotlinx.coroutines.android) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") - - implementation("com.googlecode.libphonenumber:libphonenumber:9.0.26") + implementation(libs.libphonenumber) implementation(project(":lib:platform_frameworks_opt_chips")) implementation(project(":lib:platform_frameworks_opt_photoviewer")) implementation(project(":lib:platform_frameworks_opt_vcard")) - debugImplementation("androidx.compose.ui:ui-test-manifest") - debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) - androidTestImplementation(composeBom) - androidTestImplementation("androidx.compose.ui:ui-test-junit4") + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) } diff --git a/build.gradle.kts b/build.gradle.kts index 25cc7e78..78fe17e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,11 @@ plugins { - id("com.android.application") version "9.1.0" apply false - id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.compose) apply false } buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.20") - classpath("com.google.devtools.ksp:symbol-processing-gradle-plugin:2.3.6") + classpath(libs.kotlin.gradle.plugin) + classpath(libs.ksp.gradle.plugin) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..1353d489 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,62 @@ +[versions] +agp = "9.1.0" +kotlin = "2.3.20" +ksp = "2.3.6" + +activity-compose = "1.13.0" +appcompat = "1.7.1" +coil = "3.4.0" +compose-bom = "2026.03.00" +coroutines = "1.10.2" +glide = "5.0.5" +guava = "33.5.0-android" +jsr305 = "3.0.2" +libphonenumber = "9.0.26" +lifecycle = "2.10.0" +palette = "1.0.0" +preference = "1.2.1" +recyclerview = "1.4.0" + +[libraries] +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } + +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } + +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } + +androidx-palette = { module = "androidx.palette:palette", version.ref = "palette" } +androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } + +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } + +guava = { module = "com.google.guava:guava", version.ref = "guava" } +jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } + +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } + +libphonenumber = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "libphonenumber" } + +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +ksp-gradle-plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } + +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } From 4635630d35bd0479ceaacc49fa4a51959434a32c Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Fri, 20 Mar 2026 15:59:45 +0200 Subject: [PATCH 07/15] Add Hilt --- app/build.gradle.kts | 11 +- build.gradle.kts | 3 + gradle/libs.versions.toml | 9 + gradle/verification-metadata.xml | 307 ++++++++++++++++++ .../android/messaging/BugleApplication.java | 3 + 5 files changed, 332 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8180ba7f..3f5f4406 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,7 +3,9 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) + alias(libs.plugins.hilt) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) } java { @@ -93,6 +95,8 @@ android { } dependencies { + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) implementation(libs.androidx.appcompat) implementation(libs.androidx.palette) implementation(libs.androidx.preference) @@ -115,11 +119,13 @@ dependencies { implementation(libs.coil.compose) implementation(libs.glide) + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + implementation(libs.guava) implementation(libs.jsr305) implementation(libs.kotlinx.coroutines.android) - implementation(libs.libphonenumber) implementation(project(":lib:platform_frameworks_opt_chips")) @@ -131,4 +137,7 @@ dependencies { androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) + + androidTestImplementation(libs.hilt.android.testing) + kspAndroidTest(libs.hilt.compiler) } diff --git a/build.gradle.kts b/build.gradle.kts index 78fe17e7..de88f953 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,13 @@ plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.hilt) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.ksp) apply false } buildscript { dependencies { + classpath(libs.hilt.gradle.plugin) classpath(libs.kotlin.gradle.plugin) classpath(libs.ksp.gradle.plugin) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1353d489..a3ef6f59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,8 @@ agp = "9.1.0" kotlin = "2.3.20" ksp = "2.3.6" +hilt = "2.59.2" + activity-compose = "1.13.0" appcompat = "1.7.1" coil = "3.4.0" @@ -47,10 +49,15 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } guava = { module = "com.google.guava:guava", version.ref = "guava" } jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } + kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } libphonenumber = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "libphonenumber" } +hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } ksp-gradle-plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" } @@ -58,5 +65,7 @@ ksp-gradle-plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } + kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index dc5f669c..d8e421da 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5223,6 +5223,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/messaging/BugleApplication.java b/src/com/android/messaging/BugleApplication.java index 8470ca19..5afe8fb7 100644 --- a/src/com/android/messaging/BugleApplication.java +++ b/src/com/android/messaging/BugleApplication.java @@ -47,9 +47,12 @@ import androidx.appcompat.mms.CarrierConfigValuesLoader; import androidx.appcompat.mms.MmsManager; +import dagger.hilt.android.HiltAndroidApp; + /** * The application object */ +@HiltAndroidApp public class BugleApplication extends Application implements UncaughtExceptionHandler { private static final String TAG = LogUtil.BUGLE_TAG; From 63b6412bcc0d8c9ca4b210759c4917da63535fba Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Fri, 20 Mar 2026 16:00:22 +0200 Subject: [PATCH 08/15] Add submodules mappings to vcs.xml --- .idea/vcs.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 8f4b0919..4d760f2f 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -10,5 +10,9 @@ + + + + - + \ No newline at end of file From 3907bc2c03a6defc5c509be6ee0db18fbb99d4f4 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Tue, 24 Mar 2026 18:08:55 +0200 Subject: [PATCH 09/15] Provide coroutine dispatchers and content resolver with DI --- .../messaging/di/core/CoreProvidesModule.kt | 47 +++++++++++++++++++ .../android/messaging/di/core/Qualifiers.kt | 15 ++++++ 2 files changed, 62 insertions(+) create mode 100644 src/com/android/messaging/di/core/CoreProvidesModule.kt create mode 100644 src/com/android/messaging/di/core/Qualifiers.kt diff --git a/src/com/android/messaging/di/core/CoreProvidesModule.kt b/src/com/android/messaging/di/core/CoreProvidesModule.kt new file mode 100644 index 00000000..8be96507 --- /dev/null +++ b/src/com/android/messaging/di/core/CoreProvidesModule.kt @@ -0,0 +1,47 @@ +package com.android.messaging.di.core + +import android.content.ContentResolver +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +@Module +@InstallIn(SingletonComponent::class) +internal class CoreProvidesModule { + + @Provides + @Reusable + @DefaultDispatcher + fun provideDefaultDispatcher(): CoroutineDispatcher { + return Dispatchers.Default + } + + @Provides + @Reusable + @IoDispatcher + fun provideIoDispatcher(): CoroutineDispatcher { + return Dispatchers.IO + } + + @Provides + @Reusable + @MainDispatcher + fun provideMainDispatcher(): CoroutineDispatcher { + return Dispatchers.Main + } + + @Provides + @Reusable + fun provideContentResolver( + @ApplicationContext + context: Context, + ): ContentResolver { + return context.contentResolver + } +} diff --git a/src/com/android/messaging/di/core/Qualifiers.kt b/src/com/android/messaging/di/core/Qualifiers.kt new file mode 100644 index 00000000..4bf9b1ec --- /dev/null +++ b/src/com/android/messaging/di/core/Qualifiers.kt @@ -0,0 +1,15 @@ +package com.android.messaging.di.core + +import javax.inject.Qualifier + +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class DefaultDispatcher + +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class IoDispatcher + +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class MainDispatcher From dccd42004c51cd02a4f115dd6c657dd24f493554 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Tue, 24 Mar 2026 18:09:04 +0200 Subject: [PATCH 10/15] Add Compose theme --- res/values/styles.xml | 1 + src/com/android/messaging/ui/core/Theme.kt | 24 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/com/android/messaging/ui/core/Theme.kt diff --git a/res/values/styles.xml b/res/values/styles.xml index c7f8abae..fa5884d4 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -23,6 +23,7 @@ true +