Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
b1b9889
Add conversation messages repository
RankoR Mar 24, 2026
7681405
Add Compose conversation message models and list components
RankoR Mar 24, 2026
04eeb3b
Wire Compose conversation screen and activity
RankoR Mar 24, 2026
bc5f780
Add conversation metadata loading and improve conversation UI
RankoR Mar 24, 2026
ee067ff
Add Kotlin Flow extensions
RankoR Mar 25, 2026
b82f75b
Improve Conversation screen package structure
RankoR Mar 26, 2026
eecbae6
Add draft/composer state and delegate architecture
RankoR Mar 27, 2026
a051735
Wire conversation Compose UI to new state
RankoR Mar 27, 2026
df6bfa3
Composer draft send flow
RankoR Mar 28, 2026
f87a51e
Compose bar and screen behavior improvements
RankoR Mar 28, 2026
48a3ad3
Ignore .log files in Git
RankoR Mar 28, 2026
9cb3771
Add dependencies for media picker
RankoR Mar 28, 2026
deb94f3
Update Theme to better match Material Expressive shapes
RankoR Apr 1, 2026
8f99420
Add parcelize plugin
RankoR Apr 2, 2026
cfb759d
Update ktlint rules
RankoR Apr 2, 2026
61b2038
Add immutable Kotlin collections dependency
RankoR Apr 7, 2026
bd1c6a2
Refactor conversation draft composer state
RankoR Apr 2, 2026
5371109
Add conversation media picker implementation
RankoR Apr 2, 2026
5dae620
Wire conversation screen to media picker
RankoR Apr 2, 2026
0fc207a
Clean up conversation message and metadata mapping
RankoR Apr 2, 2026
0ff0fb1
Polish image and cursor utilities
RankoR Apr 2, 2026
dbc4674
Add conversation Compose UI test hooks
RankoR Apr 9, 2026
0ad23c7
Expand debug conversation seed data
RankoR Apr 14, 2026
27bb038
Add MMS subject sanitizer
RankoR Apr 14, 2026
6f6f31d
Fix invalid ktlint rules
RankoR Apr 14, 2026
a9abb6e
Extract ConversationMessageDataDraftMapper and reuse it in draft repo…
RankoR Apr 14, 2026
cabb918
Handle v2 conversation launch requests and startup draft seeding
RankoR Apr 14, 2026
9ad0c3c
Add Compose Navigation dependencies
RankoR Apr 14, 2026
a3a2465
Migrate to basic Compose navigation
RankoR Apr 14, 2026
78d6b98
Introduce conversation entry session model
RankoR Apr 14, 2026
13bb9c0
Use ConversationDraft for compose draft handoff
RankoR Apr 14, 2026
77956c0
Add a separate build type for performance validation
RankoR Apr 16, 2026
06b1761
Add recipient picker search stack
RankoR Apr 16, 2026
534d58b
Wire recipient picker into new chat flow
RankoR Apr 16, 2026
8dd4d4a
Add inline group creation flow to new chat
RankoR Apr 16, 2026
0f32146
Extract shared recipient selection UI and delegate
RankoR Apr 17, 2026
3d404f7
Add conversation navigation reducer
RankoR Apr 17, 2026
0bfa12c
Add add-participants conversation flow
RankoR Apr 17, 2026
5851be7
Open conversation detail screen on top bar click
RankoR Apr 17, 2026
79fea46
Extract message selection into delegate
RankoR Apr 17, 2026
9182fca
Add compose multi-message selection UI
RankoR Apr 17, 2026
edd3074
Simplify nullable scope launch in recipient picker
RankoR Apr 17, 2026
31aa882
Add calls from the conversation screen
RankoR Apr 20, 2026
e5f8ea1
Implement more conversation actions
RankoR Apr 20, 2026
53d7c96
Add conversation subscription repository and debug SIM emulation
RankoR Apr 20, 2026
5dc6f74
Add conversation SIM selection from overflow menu
RankoR Apr 20, 2026
413dbc4
Display avatars in conversation top bar
RankoR Apr 20, 2026
7de46b6
Ignore .kotlin
RankoR Apr 20, 2026
5eddd41
Show 1:1 phone-number subtitle for conversation
RankoR Apr 20, 2026
a2c5d00
Add audio attachments UI and playback
RankoR Apr 21, 2026
1dad16b
Add vCard support for inline attachments
RankoR Apr 21, 2026
bb76b64
Add richer attachment handling for conversation compose UI
RankoR Apr 22, 2026
654b3f6
Remove contacts picker from back stack after navigation to conversation
RankoR Apr 22, 2026
b63cc31
Add audio recording attachments
RankoR Apr 22, 2026
d4e379f
Improve phone country code detection
RankoR Apr 23, 2026
65931a3
Add ability to start a chat with a phone number, not only a contact
RankoR Apr 23, 2026
068302b
Audio recording lock
RankoR Apr 24, 2026
eae89cc
Add audio recording attachment to the attachments menu
RankoR Apr 24, 2026
e83658e
Handle new messages and notifications in Compose conversation
RankoR Apr 26, 2026
9474010
Add ability to download attachments
RankoR Apr 27, 2026
be758ee
Scroll to a message index from Intent on Compose screen
RankoR Apr 27, 2026
edfdec1
Add "new message received" snackbar
RankoR Apr 27, 2026
7540323
Prevent calling on emergency numbers
RankoR Apr 27, 2026
67fad39
Add default SMS app prompt
RankoR Apr 27, 2026
9d604c3
Organize conversation data and use case packages
RankoR Apr 27, 2026
4623cee
Add conversation draft send protocol use case
RankoR Apr 27, 2026
872bf62
Harden conversation draft sending
RankoR Apr 27, 2026
6090aaf
Show error message on draft validation failure
RankoR Apr 27, 2026
4150c5a
Resend failed message on tap
RankoR Apr 28, 2026
e37f8fe
Don't hide keyboard when attachments menu is shown
RankoR Apr 28, 2026
b0121db
Add conversation action button previews and improve readability
RankoR Apr 28, 2026
b7000c1
Do not hide keyboard when starting recording audio
RankoR Apr 28, 2026
76a8df4
Add compose bar previews
RankoR Apr 28, 2026
24562bb
Add MMS indication in message composer
RankoR Apr 28, 2026
48f1c46
Add photo picker draft attachment plumbing
RankoR Apr 29, 2026
5b47315
Use Embedded Photo Picker for conversation media
RankoR Apr 29, 2026
404148d
Keep media review captions stable while editing
RankoR Apr 29, 2026
8a7d6b6
Expose MMS indicator test tag in semantics
RankoR Apr 29, 2026
2078fa4
Disable ForbiddenComment rule in Detekt
RankoR Apr 29, 2026
702bbda
Adjust Detekt functions count rules to the reality
RankoR Apr 29, 2026
a01ba62
Fix TooManyFunctions errors
RankoR Apr 29, 2026
5972182
Fix LongMethod errors
RankoR Apr 30, 2026
6b858d9
Suppress TooGenericExceptionCaught where generic exceptions are needed
RankoR Apr 30, 2026
68a29cd
Fix TooManyFunctions error
RankoR Apr 30, 2026
9ff3b5e
Fix MagicNumbers
RankoR Apr 30, 2026
ce231e7
Fix MatchingDeclarationName errors
RankoR Apr 30, 2026
7b0f3f7
Fix LoopWithTooManyJumpStatements errors
RankoR Apr 30, 2026
12c8006
Suppress false-positive CyclomaticComplexMethod errors
RankoR Apr 30, 2026
a8e196a
Fix ReturnCount detekt errors
RankoR Apr 30, 2026
9c95f06
Improve packages organization
RankoR May 1, 2026
3b0384d
Update dependencies
RankoR May 1, 2026
da254fc
Delete old converation screen code and make the new one default
RankoR May 1, 2026
64f50e8
Fix ktlint error
RankoR May 1, 2026
b8ca40c
Fix stale MMS detection after attachments removed
RankoR May 1, 2026
a29951c
Fix blank self participant id handling for drafts
RankoR May 6, 2026
7375ae5
Fix long-press on messages containing links
RankoR May 6, 2026
dc6e77f
Fix link colors for selected messages
RankoR May 6, 2026
afe6aae
Don't show phone number for known contacts in conversation
RankoR May 6, 2026
8d25e33
Don't show name/number in 1-1 conversations
RankoR May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_packages_to_use_import_on_demand = unset
ij_kotlin_line_break_after_multiline_when_entry = false
ktlint_code_style = android_studio
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_filename = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
max_line_length = 100
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ keystore.properties
*.keystore
local.properties
/lib/build

.kotlin

*.log
2 changes: 1 addition & 1 deletion AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
android:allowEmbedded="true"
android:resizeableActivity="true"
android:windowSoftInputMode="stateHidden|adjustResize"
android:theme="@style/BugleTheme.ConversationActivity"
android:theme="@style/Theme.Compose"
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
Expand Down
31 changes: 31 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
}

Expand Down Expand Up @@ -45,6 +47,7 @@ android {
versionName = "13"
minSdk = 35
targetSdk = 35
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

ndk {
abiFilters.clear()
Expand Down Expand Up @@ -111,6 +114,14 @@ android {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "Messaging d")
}

create("perf") {
initWith(getByName("release"))
applicationIdSuffix = ".debug"
matchingFallbacks += listOf("release")
resValue("string", "app_name", "Messaging d")
signingConfig = signingConfigs.getByName("debug")
}
}

lint {
Expand All @@ -120,6 +131,13 @@ android {

dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.compose)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.video)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.palette)
implementation(libs.androidx.preference)
implementation(libs.androidx.recyclerview)
Expand All @@ -132,13 +150,21 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)

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(libs.androidx.lifecycle.viewmodel.navigation3)

implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)

implementation(libs.androidx.photo.picker)

implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
implementation(libs.glide)

implementation(libs.hilt.android)
Expand All @@ -147,7 +173,9 @@ dependencies {
implementation(libs.guava)
implementation(libs.jsr305)

implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.core)

implementation(libs.libphonenumber)

Expand All @@ -168,6 +196,9 @@ dependencies {

androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.runner)

androidTestImplementation(libs.hilt.android.testing)
kspAndroidTest(libs.hilt.compiler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.android.messaging.ui.conversation.messages.ui.message

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import com.android.messaging.ui.conversation.messages.model.message.ConversationMessagePartUiModel
import com.android.messaging.ui.conversation.messages.model.message.ConversationMessageUiModel
import com.android.messaging.ui.core.AppTheme
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

private const val MESSAGE_ID = "message-id"
private const val CONVERSATION_ID = "conversation-id"
private const val LINK_ONLY_TEXT = "https://example.com"
private const val PLAIN_TEXT = "plain outgoing message"
private const val TIMESTAMP = 1_700_000_000_000L

internal class ConversationMessageLinkLongClickTest {
@get:Rule
val composeRule = createComposeRule()

@Test
fun longClickOutgoingLinkOnlyMessageSelectsMessage() {
var externalUriClickCount = 0
var messageLongClickCount = 0

composeRule.setContent {
AppTheme {
ConversationMessage(
message = outgoingMessage(text = LINK_ONLY_TEXT),
onExternalUriClick = {
externalUriClickCount += 1
},
onMessageLongClick = {
messageLongClickCount += 1
},
)
}
}

composeRule.waitForIdle()

composeRule
.onNodeWithText(text = LINK_ONLY_TEXT, useUnmergedTree = true)
.performClick()

composeRule.runOnIdle {
assertEquals(1, externalUriClickCount)
}

composeRule
.onNodeWithText(text = LINK_ONLY_TEXT, useUnmergedTree = true)
.performTouchInput {
longClick(position = center)
}

composeRule.runOnIdle {
assertEquals(1, externalUriClickCount)
assertEquals(1, messageLongClickCount)
}
}

@Test
fun longClickOutgoingLinkOnlyMessageStaysSelectedAfterRelease() {
var externalUriClickCount = 0
var messageClickCount = 0
var messageLongClickCount = 0
var isSelected by mutableStateOf(false)
var isSelectionMode by mutableStateOf(false)

composeRule.setContent {
AppTheme {
ConversationMessage(
message = outgoingMessage(text = LINK_ONLY_TEXT),
isSelected = isSelected,
isSelectionMode = isSelectionMode,
onExternalUriClick = {
externalUriClickCount += 1
},
onMessageClick = {
messageClickCount += 1
isSelected = !isSelected
},
onMessageLongClick = {
messageLongClickCount += 1
isSelectionMode = true
isSelected = true
},
)
}
}

composeRule.waitForIdle()

composeRule
.onNodeWithText(text = LINK_ONLY_TEXT, useUnmergedTree = true)
.performTouchInput {
longClick(position = center)
}

composeRule.runOnIdle {
assertEquals(0, externalUriClickCount)
assertEquals(0, messageClickCount)
assertEquals(1, messageLongClickCount)
assertEquals(true, isSelected)
assertEquals(true, isSelectionMode)
}
}

@Test
fun longClickOutgoingPlainTextMessageSelectsMessageOnce() {
var messageLongClickCount = 0

composeRule.setContent {
AppTheme {
ConversationMessage(
message = outgoingMessage(text = PLAIN_TEXT),
onMessageLongClick = {
messageLongClickCount += 1
},
)
}
}

composeRule.waitForIdle()

composeRule
.onNodeWithText(text = PLAIN_TEXT, useUnmergedTree = true)
.performTouchInput {
longClick(position = center)
}

composeRule.runOnIdle {
assertEquals(1, messageLongClickCount)
}
}
}

private fun outgoingMessage(text: String): ConversationMessageUiModel {
return ConversationMessageUiModel(
messageId = MESSAGE_ID,
conversationId = CONVERSATION_ID,
text = text,
parts = listOf(
ConversationMessagePartUiModel.Text(
text = text,
),
),
sentTimestamp = TIMESTAMP,
receivedTimestamp = TIMESTAMP,
displayTimestamp = TIMESTAMP,
status = ConversationMessageUiModel.Status.Outgoing.Complete,
isIncoming = false,
senderDisplayName = null,
senderAvatarUri = null,
senderContactLookupKey = null,
canClusterWithPrevious = false,
canClusterWithNext = false,
canCopyMessageToClipboard = true,
canDownloadMessage = false,
canForwardMessage = true,
canResendMessage = false,
canSaveAttachments = false,
mmsSubject = null,
protocol = ConversationMessageUiModel.Protocol.SMS,
)
}
Binary file added app/src/debug/assets/seed_video.mp4
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.android.messaging.data.conversation.store

import com.android.messaging.datamodel.DataModel
import com.android.messaging.datamodel.DatabaseWrapper
import com.android.messaging.datamodel.data.ConversationListItemData
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test

internal class ConversationDraftStoreTest {

private val databaseWrapper = mockk<DatabaseWrapper>()
private val dataModel = mockk<DataModel>()

private val store = ConversationDraftStoreImpl()

@Before
fun setUp() {
mockkStatic(DataModel::class)
mockkStatic(ConversationListItemData::class)

every { DataModel.get() } returns dataModel
every { dataModel.database } returns databaseWrapper
}

@After
fun tearDown() {
unmockkAll()
}

@Test
fun getSelfParticipantIdReturnsNullWhenConversationSelfIdIsMissing() {
val conversation = ConversationListItemData()
every {
ConversationListItemData.getExistingConversation(databaseWrapper, CONVERSATION_ID)
} returns conversation

val selfParticipantId = store.getSelfParticipantId(
conversationId = CONVERSATION_ID,
)

assertNull(selfParticipantId)
}

private companion object {
private const val CONVERSATION_ID = "conversation-id"
}
}
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.ktlint)
}
Expand Down
9 changes: 9 additions & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ complexity:
LongParameterList:
ignoreDefaultParameters: true
TooManyFunctions:
allowedFunctionsPerClass: 60
allowedFunctionsPerFile: 15
allowedFunctionsPerInterface: 50
ignoreAnnotatedFunctions:
- Preview

Expand All @@ -15,9 +18,15 @@ naming:
- Composable

style:
ForbiddenComment:
active: false

MagicNumber:
ignoreCompanionObjectPropertyDeclaration: true
ignorePropertyDeclaration: true
ignoreAnnotated:
- Composable

UnusedPrivateFunction:
ignoreAnnotated:
- Preview
Loading