Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 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
70b5b3b
Add a "checkbox" indicator to the message in the selection mode
RankoR May 7, 2026
99b7c7d
Fix links colors
RankoR May 7, 2026
e3f5def
Validate MMS attachments limit before sending and attaching
RankoR May 8, 2026
71a2383
Add support for MMS subject
RankoR May 9, 2026
ce28d61
Show SIM selection bottom sheet on Send button long press
RankoR May 9, 2026
e98e855
Improve SIM selection item in the conversation overflow menu
RankoR May 9, 2026
f6cd4a0
SIM indication for messages
RankoR May 9, 2026
6d0dcd6
Use cutoff timestamp when deleting conversations
RankoR May 11, 2026
ee68016
Perform checks before deleting conversations
RankoR May 11, 2026
4eb6195
Add message segment and chars counter
RankoR May 11, 2026
66da823
Group multiple phone and email destinations for same contact and form…
RankoR May 12, 2026
826f176
Auto focus query field in NewChatScreen
RankoR May 12, 2026
8bf8703
Add SIM chooser to new chat screen and improve packages organization
RankoR May 12, 2026
6214890
Fix YetToSend status mapping
RankoR May 12, 2026
f4b1b31
Add sending message announcement and sound
RankoR May 12, 2026
c6773d9
MMS downloading UI
RankoR May 13, 2026
60fe0bf
Set minSdk to 36 and add comment regarding U SDK Extensions
RankoR May 13, 2026
34f9932
Add a targetSdk 35 clarification comment
RankoR May 13, 2026
94df01e
Fix ktlint indentation issue
RankoR May 13, 2026
473ff92
Upgrade dependencies
RankoR May 13, 2026
0f76650
Fix attachments menu position
RankoR May 13, 2026
7a42ea4
Add avatars for group conversations
RankoR May 13, 2026
d01c2e4
Replace fakes with mocks
RankoR May 13, 2026
ec5058d
Fix deps pinning
RankoR May 13, 2026
a174c5e
Fix incorrect send button long press handling
RankoR May 13, 2026
35f87c3
Fix message bubble resize on selection when font size is small
RankoR May 14, 2026
dff7a2c
Properly disable message input field when recording audio
RankoR May 14, 2026
60b1ab9
Display avatar for attached contacts
RankoR May 14, 2026
409a22d
Reduce attachments spacing
RankoR May 14, 2026
9447cb4
Improve message selection indicator layout and spacing
RankoR May 14, 2026
943db6b
Replace legacy "stop recording" icon with a Compose one
RankoR May 14, 2026
91154f6
Remove unused legacy code
RankoR May 14, 2026
0f9b472
Fix camera permission overlay covered by camera controls
RankoR May 14, 2026
3ced04b
Fix media picker overlay colors in dark mode
RankoR May 14, 2026
51d6f80
Fix audio recording state issue when audio permission is not granted
RankoR May 14, 2026
93e579b
Round conversation content corners
RankoR May 14, 2026
c5c2093
Fix top bar touch area size
RankoR May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
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
34 changes: 33 additions & 1 deletion 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 @@ -43,8 +45,10 @@ android {
defaultConfig {
versionCode = 20000000 + 13
versionName = "13"
minSdk = 35
minSdk = 36
// Do not upgrade until Compose migration finished to prevent edge-to-edge issues
targetSdk = 35
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

ndk {
abiFilters.clear()
Expand Down Expand Up @@ -111,6 +115,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 +132,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 +151,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 +174,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 +197,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,245 @@
package com.android.messaging.ui.conversation.messages.ui.message

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.Density
import com.android.messaging.datamodel.data.ParticipantData
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 HEIGHT_ASSERTION_DELTA_DP = 0.5f
private const val LINK_ONLY_TEXT = "https://example.com"
private const val MESSAGE_TEST_TAG = "conversation-message"
private const val MINIMAL_FONT_SCALE = 0.85f
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)
}
}

@Test
fun selectionModeDoesNotChangePlainTextMessageHeightAtSmallFontScale() {
var isSelected by mutableStateOf(false)
var isSelectionMode by mutableStateOf(false)

composeRule.setContent {
val density = LocalDensity.current

CompositionLocalProvider(
LocalDensity provides Density(
density = density.density,
fontScale = MINIMAL_FONT_SCALE,
),
) {
AppTheme {
ConversationMessage(
modifier = Modifier.testTag(tag = MESSAGE_TEST_TAG),
message = outgoingMessage(text = PLAIN_TEXT),
isSelected = isSelected,
isSelectionMode = isSelectionMode,
)
}
}
}

composeRule.waitForIdle()

val unselectedHeight = composeRule
.onNodeWithTag(testTag = MESSAGE_TEST_TAG)
.getUnclippedBoundsInRoot()
.let { bounds ->
bounds.bottom - bounds.top
}

composeRule.runOnIdle {
isSelected = true
isSelectionMode = true
}
composeRule.waitForIdle()

val selectedHeight = composeRule
.onNodeWithTag(testTag = MESSAGE_TEST_TAG)
.getUnclippedBoundsInRoot()
.let { bounds ->
bounds.bottom - bounds.top
}

composeRule.runOnIdle {
assertEquals(
unselectedHeight.value,
selectedHeight.value,
HEIGHT_ASSERTION_DELTA_DP,
)
}
}
}

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,
senderContactId = ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED,
senderContactLookupKey = null,
senderNormalizedDestination = null,
senderParticipantId = null,
selfParticipantId = null,
canClusterWithPrevious = false,
canClusterWithNext = false,
canCopyMessageToClipboard = true,
canDownloadMessage = false,
canForwardMessage = true,
canResendMessage = false,
canSaveAttachments = false,
mmsDownload = null,
mmsSubject = null,
protocol = ConversationMessageUiModel.Protocol.SMS,
)
}
Loading