From e86d5baf910d18d1756c255534331ff828b5bbd3 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Sun, 31 May 2026 21:09:50 +0200 Subject: [PATCH 1/9] Add share intent screen basic components --- .../ShareIntentViewModelBindsModule.kt | 20 ++++++ .../ui/shareintent/ShareIntentActivity.kt | 57 ++++++++++++++++ .../shareintent/common/ShareIntentTokens.kt | 17 +++++ .../common/ShareIntentTopAppBar.kt | 46 +++++++++++++ .../screen/ShareIntentEffectHandler.kt | 16 +++++ .../shareintent/screen/ShareIntentScreen.kt | 68 +++++++++++++++++++ .../screen/ShareIntentViewModel.kt | 40 +++++++++++ .../screen/delegate/ShareIntentDelegate.kt | 35 ++++++++++ .../screen/model/ShareIntentAction.kt | 3 + .../screen/model/ShareIntentScreenEffect.kt | 3 + .../screen/model/ShareIntentUiState.kt | 16 +++++ 11 files changed, 321 insertions(+) create mode 100644 src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt create mode 100644 src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt create mode 100644 src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt create mode 100644 src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt create mode 100644 src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt create mode 100644 src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt create mode 100644 src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt create mode 100644 src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt create mode 100644 src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt create mode 100644 src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt create mode 100644 src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt diff --git a/src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt b/src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt new file mode 100644 index 000000000..4a501591d --- /dev/null +++ b/src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt @@ -0,0 +1,20 @@ +package com.android.messaging.di.shareintent + +import com.android.messaging.ui.shareintent.screen.delegate.ShareIntentScreenDelegate +import com.android.messaging.ui.shareintent.screen.delegate.ShareIntentScreenDelegateImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal abstract class ShareIntentViewModelBindsModule { + + @Binds + @ViewModelScoped + abstract fun bindShareIntentScreenDelegate( + impl: ShareIntentScreenDelegateImpl, + ): ShareIntentScreenDelegate +} diff --git a/src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt b/src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt new file mode 100644 index 000000000..e0ef4beb5 --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt @@ -0,0 +1,57 @@ +package com.android.messaging.ui.shareintent + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.android.messaging.ui.UIIntents +import com.android.messaging.ui.core.AppTheme +import com.android.messaging.ui.shareintent.screen.ShareIntentEffectHandlerImpl +import com.android.messaging.ui.shareintent.screen.ShareIntentScreen +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ShareIntentActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (redirectToSendToIfNeeded()) { + return + } + + enableEdgeToEdge() + + setContent { + AppTheme { + val effectHandler = ShareIntentEffectHandlerImpl( + activity = this, + ) + + ShareIntentScreen( + effectHandler = effectHandler, + onNavigateBack = ::finish, + ) + } + } + } + + private fun redirectToSendToIfNeeded(): Boolean { + val hasNoDestination = intent.getStringExtra("address").isNullOrEmpty() && + intent.getStringExtra(Intent.EXTRA_EMAIL).isNullOrEmpty() + + if (Intent.ACTION_SEND != intent.action || hasNoDestination) { + return false + } + + val convIntent = UIIntents.get().getLaunchConversationActivityIntent(this).apply { + putExtras(intent) + action = Intent.ACTION_SENDTO + setDataAndType(intent.data, intent.type) + } + startActivity(convIntent) + finish() + return true + } +} diff --git a/src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt b/src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt new file mode 100644 index 000000000..6f2153ca1 --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt @@ -0,0 +1,17 @@ +package com.android.messaging.ui.shareintent.common + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.unit.dp + +private val ZeroCornerSize = CornerSize(0.dp) + +internal val MaterialTheme.contentSurfaceShape: CornerBasedShape + @Composable @ReadOnlyComposable + get() = shapes.large.copy( + bottomStart = ZeroCornerSize, + bottomEnd = ZeroCornerSize, + ) diff --git a/src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt b/src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt new file mode 100644 index 000000000..c35a4278f --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt @@ -0,0 +1,46 @@ +package com.android.messaging.ui.shareintent.common + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import com.android.messaging.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ShareIntentTopAppBar( + onClose: () -> Unit, +) { + TopAppBar( + title = { + Text( + text = stringResource(R.string.share_intent_activity_label), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = stringResource(R.string.share_cancel), + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onSurface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + ), + ) +} diff --git a/src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt b/src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt new file mode 100644 index 000000000..8ed5fc38d --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt @@ -0,0 +1,16 @@ +package com.android.messaging.ui.shareintent.screen + +import android.app.Activity +import com.android.messaging.ui.shareintent.screen.model.ShareIntentScreenEffect as Effect + +internal interface ShareIntentEffectHandler { + fun handle(effect: Effect) +} + +internal class ShareIntentEffectHandlerImpl( + private val activity: Activity, +) : ShareIntentEffectHandler { + + override fun handle(effect: Effect) { + } +} diff --git a/src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt b/src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt new file mode 100644 index 000000000..c19655513 --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt @@ -0,0 +1,68 @@ +package com.android.messaging.ui.shareintent.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.messaging.ui.shareintent.common.ShareIntentTopAppBar +import com.android.messaging.ui.shareintent.common.contentSurfaceShape +import com.android.messaging.ui.shareintent.screen.model.ShareIntentAction as Action +import com.android.messaging.ui.shareintent.screen.model.ShareIntentUiState as State + +@Composable +internal fun ShareIntentScreen( + effectHandler: ShareIntentEffectHandler, + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, + screenModel: ShareIntentScreenModel = viewModel(), +) { + val uiState by screenModel.uiState.collectAsStateWithLifecycle() + + val currentEffectHandler by rememberUpdatedState(effectHandler) + LaunchedEffect(screenModel) { + screenModel.effects.collect { effect -> + currentEffectHandler.handle(effect) + } + } + + ShareIntentContent( + uiState = uiState, + onAction = screenModel::onAction, + onNavigateBack = onNavigateBack, + modifier = modifier, + ) +} + +@Composable +private fun ShareIntentContent( + uiState: State, + onAction: (Action) -> Unit, + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + topBar = { + ShareIntentTopAppBar(onClose = onNavigateBack) + }, + ) { contentPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = contentPadding.calculateTopPadding()) + .clip(MaterialTheme.contentSurfaceShape) + .background(MaterialTheme.colorScheme.background), + ) + } +} diff --git a/src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt b/src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt new file mode 100644 index 000000000..54806575c --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt @@ -0,0 +1,40 @@ +package com.android.messaging.ui.shareintent.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android.messaging.ui.shareintent.screen.delegate.ShareIntentScreenDelegate +import com.android.messaging.ui.shareintent.screen.model.ShareIntentAction as Action +import com.android.messaging.ui.shareintent.screen.model.ShareIntentScreenEffect as Effect +import com.android.messaging.ui.shareintent.screen.model.ShareIntentUiState as State +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow + +internal interface ShareIntentScreenModel { + val effects: Flow + val uiState: StateFlow + + fun onAction(action: Action) +} + +@HiltViewModel +internal class ShareIntentViewModel @Inject constructor( + delegate: ShareIntentScreenDelegate, +) : ViewModel(), + ShareIntentScreenModel { + + private val _effects = MutableSharedFlow(extraBufferCapacity = 1) + override val effects: Flow = _effects.asSharedFlow() + + override val uiState: StateFlow = delegate.state + + init { + delegate.bind(viewModelScope) + } + + override fun onAction(action: Action) { + } +} diff --git a/src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt b/src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt new file mode 100644 index 000000000..0592007dd --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt @@ -0,0 +1,35 @@ +package com.android.messaging.ui.shareintent.screen.delegate + +import com.android.messaging.di.core.DefaultDispatcher +import com.android.messaging.ui.shareintent.screen.model.ShareIntentUiState as State +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +internal interface ShareIntentScreenDelegate { + val state: StateFlow + fun bind(scope: CoroutineScope) +} + +internal class ShareIntentScreenDelegateImpl @Inject constructor( + @param:DefaultDispatcher + private val defaultDispatcher: CoroutineDispatcher, +) : ShareIntentScreenDelegate { + + private val _state = MutableStateFlow(State()) + override val state: StateFlow = _state.asStateFlow() + + private var isBound = false + + override fun bind(scope: CoroutineScope) { + if (isBound) return + isBound = true + + scope.launch(defaultDispatcher) { + } + } +} diff --git a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt new file mode 100644 index 000000000..3f13dc46c --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt @@ -0,0 +1,3 @@ +package com.android.messaging.ui.shareintent.screen.model + +internal sealed interface ShareIntentAction diff --git a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt new file mode 100644 index 000000000..efd4ffb84 --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt @@ -0,0 +1,3 @@ +package com.android.messaging.ui.shareintent.screen.model + +internal sealed interface ShareIntentScreenEffect diff --git a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt new file mode 100644 index 000000000..55a90d796 --- /dev/null +++ b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt @@ -0,0 +1,16 @@ +package com.android.messaging.ui.shareintent.screen.model + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal data class ShareIntentUiState( + val isLoading: Boolean = true, + val targets: ImmutableList = persistentListOf(), +) + +@Immutable +internal data class ShareTargetUiState( + val conversationId: String, +) From 1b7d67ba77fd7d1de67172af27783abce6cc1c4d Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Sun, 31 May 2026 21:26:42 +0200 Subject: [PATCH 2/9] Add common components ParticipantAvatar and TwoLineListItem --- .../ui/common/components/ParticipantAvatar.kt | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/com/android/messaging/ui/common/components/ParticipantAvatar.kt diff --git a/src/com/android/messaging/ui/common/components/ParticipantAvatar.kt b/src/com/android/messaging/ui/common/components/ParticipantAvatar.kt new file mode 100644 index 000000000..95aa62f57 --- /dev/null +++ b/src/com/android/messaging/ui/common/components/ParticipantAvatar.kt @@ -0,0 +1,92 @@ +package com.android.messaging.ui.common.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import coil3.compose.AsyncImage + +@Composable +internal fun ParticipantAvatar( + avatarUri: String?, + fallbackIcon: ImageVector, + fallbackIconSize: Dp, + modifier: Modifier = Modifier, + shape: Shape = CircleShape, + isSelected: Boolean = false, +) { + val backgroundColor = when { + isSelected -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.primaryContainer + } + + Box( + modifier = modifier + .clip(shape) + .background(backgroundColor), + contentAlignment = Alignment.Center, + ) { + when { + isSelected -> { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(fallbackIconSize), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + + avatarUri.isNullOrBlank() -> { + Icon( + imageVector = fallbackIcon, + contentDescription = null, + modifier = Modifier.size(fallbackIconSize), + tint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + + else -> { + AsyncImage( + model = avatarUri, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } + } + } +} + +@Composable +internal fun ParticipantAvatar( + avatarUri: String?, + size: Dp, + modifier: Modifier = Modifier, + fallbackIconSize: Dp = size / 2, + fallbackIcon: ImageVector = Icons.Default.Person, + shape: Shape = CircleShape, + isSelected: Boolean = false, +) { + ParticipantAvatar( + avatarUri = avatarUri, + fallbackIcon = fallbackIcon, + fallbackIconSize = fallbackIconSize, + modifier = modifier.size(size), + shape = shape, + isSelected = isSelected, + ) +} From 61b55f96ef3b932f8c64909bcceeab74a17dd03d Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Tue, 9 Jun 2026 13:10:06 +0200 Subject: [PATCH 3/9] Add Compose forward message screen reusing share intent picker --- .../di/forward/ForwardBindsModule.kt | 20 +++++ .../usecase/BuildForwardConversationDraft.kt | 36 +++++++++ .../ui/forward/ForwardMessageActivity.kt | 75 +++++++++++++++++++ .../ui/forward/screen/ForwardEffectHandler.kt | 53 +++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/com/android/messaging/di/forward/ForwardBindsModule.kt create mode 100644 src/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraft.kt create mode 100644 src/com/android/messaging/ui/forward/ForwardMessageActivity.kt create mode 100644 src/com/android/messaging/ui/forward/screen/ForwardEffectHandler.kt diff --git a/src/com/android/messaging/di/forward/ForwardBindsModule.kt b/src/com/android/messaging/di/forward/ForwardBindsModule.kt new file mode 100644 index 000000000..879cda486 --- /dev/null +++ b/src/com/android/messaging/di/forward/ForwardBindsModule.kt @@ -0,0 +1,20 @@ +package com.android.messaging.di.forward + +import com.android.messaging.domain.forward.usecase.BuildForwardConversationDraft +import com.android.messaging.domain.forward.usecase.BuildForwardConversationDraftImpl +import dagger.Binds +import dagger.Module +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class ForwardBindsModule { + + @Binds + @Reusable + abstract fun bindBuildForwardConversationDraft( + impl: BuildForwardConversationDraftImpl, + ): BuildForwardConversationDraft +} diff --git a/src/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraft.kt b/src/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraft.kt new file mode 100644 index 000000000..a0b6742a6 --- /dev/null +++ b/src/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraft.kt @@ -0,0 +1,36 @@ +package com.android.messaging.domain.forward.usecase + +import com.android.messaging.data.conversation.model.draft.ConversationDraft +import com.android.messaging.data.conversation.model.draft.ConversationDraftAttachment +import com.android.messaging.datamodel.data.MessageData +import com.android.messaging.util.ContentType +import javax.inject.Inject +import kotlinx.collections.immutable.toImmutableList + +internal interface BuildForwardConversationDraft { + operator fun invoke(message: MessageData): ConversationDraft +} + +internal class BuildForwardConversationDraftImpl @Inject constructor() : + BuildForwardConversationDraft { + + override fun invoke(message: MessageData): ConversationDraft { + val attachments = message.parts + .filter { part -> + ContentType.isMediaType(part.contentType) && part.contentUri != null + } + .map { part -> + ConversationDraftAttachment( + contentType = part.contentType, + contentUri = part.contentUri.toString(), + ) + } + .toImmutableList() + + return ConversationDraft( + messageText = message.messageText, + subjectText = message.mmsSubject.orEmpty(), + attachments = attachments, + ) + } +} diff --git a/src/com/android/messaging/ui/forward/ForwardMessageActivity.kt b/src/com/android/messaging/ui/forward/ForwardMessageActivity.kt new file mode 100644 index 000000000..2fb2033f8 --- /dev/null +++ b/src/com/android/messaging/ui/forward/ForwardMessageActivity.kt @@ -0,0 +1,75 @@ +package com.android.messaging.ui.forward + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.remember +import androidx.core.content.IntentCompat +import com.android.messaging.datamodel.data.MessageData +import com.android.messaging.di.core.ApplicationCoroutineScope +import com.android.messaging.domain.forward.usecase.BuildForwardConversationDraft +import com.android.messaging.domain.shareintent.usecase.SendSharedContentToTargets +import com.android.messaging.ui.UIIntents +import com.android.messaging.ui.core.AppTheme +import com.android.messaging.ui.forward.screen.ForwardEffectHandler +import com.android.messaging.ui.shareintent.screen.ShareIntentScreen +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +@AndroidEntryPoint +class ForwardMessageActivity : ComponentActivity() { + + @Inject + @ApplicationCoroutineScope + internal lateinit var applicationScope: CoroutineScope + + @Inject + internal lateinit var sendSharedContentToTargets: SendSharedContentToTargets + + @Inject + internal lateinit var buildForwardConversationDraft: BuildForwardConversationDraft + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val message = IntentCompat.getParcelableExtra( + intent, + UIIntents.UI_INTENT_EXTRA_DRAFT_DATA, + MessageData::class.java, + ) + + if (message == null) { + finish() + return + } + + enableEdgeToEdge() + + setContent { + AppTheme { + val draft = remember(message) { + buildForwardConversationDraft(message) + } + + val effectHandler = remember(message) { + ForwardEffectHandler( + applicationScope = applicationScope, + activity = this, + message = message, + sendSharedContentToTargets = sendSharedContentToTargets, + ) + } + + ShareIntentScreen( + effectHandler = effectHandler, + onNavigateBack = ::finish, + allowMultiSelect = true, + isInitialDraftLoading = false, + initialDraft = draft, + ) + } + } + } +} diff --git a/src/com/android/messaging/ui/forward/screen/ForwardEffectHandler.kt b/src/com/android/messaging/ui/forward/screen/ForwardEffectHandler.kt new file mode 100644 index 000000000..fedef6f38 --- /dev/null +++ b/src/com/android/messaging/ui/forward/screen/ForwardEffectHandler.kt @@ -0,0 +1,53 @@ +package com.android.messaging.ui.forward.screen + +import android.app.Activity +import com.android.messaging.data.conversation.model.draft.ConversationDraft +import com.android.messaging.datamodel.data.MessageData +import com.android.messaging.domain.shareintent.model.ShareSendTarget +import com.android.messaging.domain.shareintent.usecase.SendSharedContentToTargets +import com.android.messaging.ui.UIIntents +import com.android.messaging.ui.shareintent.screen.ShareIntentEffectHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import com.android.messaging.ui.shareintent.screen.model.ShareIntentScreenEffect as Effect + +internal class ForwardEffectHandler( + private val applicationScope: CoroutineScope, + private val activity: Activity, + private val message: MessageData, + private val sendSharedContentToTargets: SendSharedContentToTargets, +) : ShareIntentEffectHandler { + + override fun handle(effect: Effect) { + when (effect) { + is Effect.OpenConversation -> { + openConversation(effect.conversationId) + } + + is Effect.SendToSelected -> { + sendToSelected(effect.targets, effect.draft) + } + } + } + + private fun openConversation(conversationId: String) { + UIIntents.get().launchConversationActivity( + activity, + conversationId, + message, + ) + activity.finish() + } + + private fun sendToSelected( + targets: Set, + draft: ConversationDraft, + ) { + applicationScope.launch { + sendSharedContentToTargets(draft, targets) + } + + UIIntents.get().launchConversationListActivity(activity) + activity.finish() + } +} From e323b242a272a9084ce7230628167a592144f275 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Tue, 9 Jun 2026 13:22:12 +0200 Subject: [PATCH 4/9] Replace forward message activity with Compose share screen --- AndroidManifest.xml | 6 +- .../android/messaging/ui/UIIntentsImpl.java | 2 +- .../ConversationListFragment.java | 8 -- .../ForwardMessageActivity.java | 84 ------------------- .../ui/forward/screen/ForwardEffectHandler.kt | 2 +- 5 files changed, 5 insertions(+), 97 deletions(-) delete mode 100644 src/com/android/messaging/ui/conversationlist/ForwardMessageActivity.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c97ea2eac..a3e28c8ea 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -247,12 +247,12 @@ - + android:theme="@style/Theme.Compose" + android:windowSoftInputMode="stateHidden|adjustResize" /> { sendToSelected(effect.targets, effect.draft) } + + is Effect.OpenAttachmentPreview -> Unit + is Effect.OpenConversationFailed -> Unit } } private fun openConversation(conversationId: String) { - UIIntents.get().launchConversationActivity( - activity, - conversationId, - message, - ) + UIIntents.get().launchConversationActivity(activity, conversationId, message) activity.finish() } private fun sendToSelected( - targets: Set, + targets: Set, draft: ConversationDraft, ) { applicationScope.launch { - sendSharedContentToTargets(draft, targets) + sendContentToTargets(draft, targets) } UIIntents.get().launchConversationListActivity(activity) From 384908ebc2235c7ceeeade23aa84e185e3c807dc Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Thu, 25 Jun 2026 02:54:14 +0200 Subject: [PATCH 7/9] Fix forward message screen after rebase --- .../ShareIntentViewModelBindsModule.kt | 20 ---- .../ui/common/components/ParticipantAvatar.kt | 92 ------------------- .../host/forward/ForwardMessageActivity.kt | 9 +- ...dler.kt => ForwardMessageEffectHandler.kt} | 38 +++++++- .../ui/shareintent/ShareIntentActivity.kt | 57 ------------ .../shareintent/common/ShareIntentTokens.kt | 17 ---- .../common/ShareIntentTopAppBar.kt | 46 ---------- .../screen/ShareIntentEffectHandler.kt | 16 ---- .../shareintent/screen/ShareIntentScreen.kt | 68 -------------- .../screen/ShareIntentViewModel.kt | 40 -------- .../screen/delegate/ShareIntentDelegate.kt | 35 ------- .../screen/model/ShareIntentAction.kt | 3 - .../screen/model/ShareIntentScreenEffect.kt | 3 - .../screen/model/ShareIntentUiState.kt | 16 ---- 14 files changed, 42 insertions(+), 418 deletions(-) delete mode 100644 src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt delete mode 100644 src/com/android/messaging/ui/common/components/ParticipantAvatar.kt rename src/com/android/messaging/ui/conversationpicker/host/forward/{ForwardMessageHandler.kt => ForwardMessageEffectHandler.kt} (55%) delete mode 100644 src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt delete mode 100644 src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt delete mode 100644 src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt delete mode 100644 src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt delete mode 100644 src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt delete mode 100644 src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt delete mode 100644 src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt delete mode 100644 src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt delete mode 100644 src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt delete mode 100644 src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt diff --git a/src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt b/src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt deleted file mode 100644 index 4a501591d..000000000 --- a/src/com/android/messaging/di/shareintent/ShareIntentViewModelBindsModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.android.messaging.di.shareintent - -import com.android.messaging.ui.shareintent.screen.delegate.ShareIntentScreenDelegate -import com.android.messaging.ui.shareintent.screen.delegate.ShareIntentScreenDelegateImpl -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -internal abstract class ShareIntentViewModelBindsModule { - - @Binds - @ViewModelScoped - abstract fun bindShareIntentScreenDelegate( - impl: ShareIntentScreenDelegateImpl, - ): ShareIntentScreenDelegate -} diff --git a/src/com/android/messaging/ui/common/components/ParticipantAvatar.kt b/src/com/android/messaging/ui/common/components/ParticipantAvatar.kt deleted file mode 100644 index 95aa62f57..000000000 --- a/src/com/android/messaging/ui/common/components/ParticipantAvatar.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.android.messaging.ui.common.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Person -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.Dp -import coil3.compose.AsyncImage - -@Composable -internal fun ParticipantAvatar( - avatarUri: String?, - fallbackIcon: ImageVector, - fallbackIconSize: Dp, - modifier: Modifier = Modifier, - shape: Shape = CircleShape, - isSelected: Boolean = false, -) { - val backgroundColor = when { - isSelected -> MaterialTheme.colorScheme.primary - else -> MaterialTheme.colorScheme.primaryContainer - } - - Box( - modifier = modifier - .clip(shape) - .background(backgroundColor), - contentAlignment = Alignment.Center, - ) { - when { - isSelected -> { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(fallbackIconSize), - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - - avatarUri.isNullOrBlank() -> { - Icon( - imageVector = fallbackIcon, - contentDescription = null, - modifier = Modifier.size(fallbackIconSize), - tint = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } - - else -> { - AsyncImage( - model = avatarUri, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize(), - ) - } - } - } -} - -@Composable -internal fun ParticipantAvatar( - avatarUri: String?, - size: Dp, - modifier: Modifier = Modifier, - fallbackIconSize: Dp = size / 2, - fallbackIcon: ImageVector = Icons.Default.Person, - shape: Shape = CircleShape, - isSelected: Boolean = false, -) { - ParticipantAvatar( - avatarUri = avatarUri, - fallbackIcon = fallbackIcon, - fallbackIconSize = fallbackIconSize, - modifier = modifier.size(size), - shape = shape, - isSelected = isSelected, - ) -} diff --git a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt index e2800c5d7..06e6d8f34 100644 --- a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt +++ b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.remember import androidx.core.content.IntentCompat import com.android.messaging.datamodel.data.MessageData import com.android.messaging.di.core.ApplicationCoroutineScope +import com.android.messaging.di.core.MainDispatcher import com.android.messaging.domain.conversationpicker.usecase.SendContentToTargets import com.android.messaging.domain.forward.usecase.BuildForwardConversationDraft import com.android.messaging.ui.UIIntents @@ -15,6 +16,7 @@ import com.android.messaging.ui.conversationpicker.ConversationPickerScreen import com.android.messaging.ui.core.AppTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @AndroidEntryPoint @@ -24,6 +26,10 @@ class ForwardMessageActivity : ComponentActivity() { @ApplicationCoroutineScope internal lateinit var applicationScope: CoroutineScope + @Inject + @MainDispatcher + internal lateinit var mainDispatcher: CoroutineDispatcher + @Inject internal lateinit var sendContentToTargets: SendContentToTargets @@ -53,8 +59,9 @@ class ForwardMessageActivity : ComponentActivity() { } val effectHandler = remember(message) { - ForwardMessageHandler( + ForwardMessageEffectHandler( applicationScope = applicationScope, + mainDispatcher = mainDispatcher, activity = this, message = message, sendContentToTargets = sendContentToTargets, diff --git a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageHandler.kt b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageEffectHandler.kt similarity index 55% rename from src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageHandler.kt rename to src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageEffectHandler.kt index 24961806b..1429b7a38 100644 --- a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageHandler.kt +++ b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageEffectHandler.kt @@ -1,18 +1,25 @@ package com.android.messaging.ui.conversationpicker.host.forward import android.app.Activity +import com.android.messaging.R import com.android.messaging.data.conversation.model.draft.ConversationDraft import com.android.messaging.datamodel.data.MessageData +import com.android.messaging.domain.conversationpicker.model.SendContentResult import com.android.messaging.domain.conversationpicker.model.SendTarget import com.android.messaging.domain.conversationpicker.usecase.SendContentToTargets import com.android.messaging.ui.UIIntents +import com.android.messaging.ui.common.components.attachment.openAttachmentPreview import com.android.messaging.ui.conversationpicker.ConversationPickerEffectHandler import com.android.messaging.ui.conversationpicker.model.ConversationPickerEffect as Effect +import com.android.messaging.util.UiUtils +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -internal class ForwardMessageHandler( +internal class ForwardMessageEffectHandler( private val applicationScope: CoroutineScope, + private val mainDispatcher: CoroutineDispatcher, private val activity: Activity, private val message: MessageData, private val sendContentToTargets: SendContentToTargets, @@ -24,12 +31,30 @@ internal class ForwardMessageHandler( openConversation(effect.conversationId) } + is Effect.OpenConversationFailed -> { + UiUtils.showToastAtBottom(R.string.conversation_picker_open_failed) + } + is Effect.SendToSelected -> { sendToSelected(effect.targets, effect.draft) } - is Effect.OpenAttachmentPreview -> Unit - is Effect.OpenConversationFailed -> Unit + is Effect.OpenAttachmentPreview -> { + openPreview(effect.contentUri, effect.contentType) + } + } + } + + private fun openPreview( + contentUri: String, + contentType: String, + ) { + applicationScope.launch(mainDispatcher) { + openAttachmentPreview( + context = activity, + contentUri = contentUri, + contentType = contentType, + ) } } @@ -43,7 +68,12 @@ internal class ForwardMessageHandler( draft: ConversationDraft, ) { applicationScope.launch { - sendContentToTargets(draft, targets) + val result = sendContentToTargets(draft, targets) + if (result is SendContentResult.Failure) { + withContext(mainDispatcher) { + UiUtils.showToastAtBottom(R.string.send_message_failure) + } + } } UIIntents.get().launchConversationListActivity(activity) diff --git a/src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt b/src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt deleted file mode 100644 index e0ef4beb5..000000000 --- a/src/com/android/messaging/ui/shareintent/ShareIntentActivity.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.android.messaging.ui.shareintent - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import com.android.messaging.ui.UIIntents -import com.android.messaging.ui.core.AppTheme -import com.android.messaging.ui.shareintent.screen.ShareIntentEffectHandlerImpl -import com.android.messaging.ui.shareintent.screen.ShareIntentScreen -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ShareIntentActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (redirectToSendToIfNeeded()) { - return - } - - enableEdgeToEdge() - - setContent { - AppTheme { - val effectHandler = ShareIntentEffectHandlerImpl( - activity = this, - ) - - ShareIntentScreen( - effectHandler = effectHandler, - onNavigateBack = ::finish, - ) - } - } - } - - private fun redirectToSendToIfNeeded(): Boolean { - val hasNoDestination = intent.getStringExtra("address").isNullOrEmpty() && - intent.getStringExtra(Intent.EXTRA_EMAIL).isNullOrEmpty() - - if (Intent.ACTION_SEND != intent.action || hasNoDestination) { - return false - } - - val convIntent = UIIntents.get().getLaunchConversationActivityIntent(this).apply { - putExtras(intent) - action = Intent.ACTION_SENDTO - setDataAndType(intent.data, intent.type) - } - startActivity(convIntent) - finish() - return true - } -} diff --git a/src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt b/src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt deleted file mode 100644 index 6f2153ca1..000000000 --- a/src/com/android/messaging/ui/shareintent/common/ShareIntentTokens.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.messaging.ui.shareintent.common - -import androidx.compose.foundation.shape.CornerBasedShape -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.unit.dp - -private val ZeroCornerSize = CornerSize(0.dp) - -internal val MaterialTheme.contentSurfaceShape: CornerBasedShape - @Composable @ReadOnlyComposable - get() = shapes.large.copy( - bottomStart = ZeroCornerSize, - bottomEnd = ZeroCornerSize, - ) diff --git a/src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt b/src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt deleted file mode 100644 index c35a4278f..000000000 --- a/src/com/android/messaging/ui/shareintent/common/ShareIntentTopAppBar.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.android.messaging.ui.shareintent.common - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import com.android.messaging.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun ShareIntentTopAppBar( - onClose: () -> Unit, -) { - TopAppBar( - title = { - Text( - text = stringResource(R.string.share_intent_activity_label), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - navigationIcon = { - IconButton(onClick = onClose) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.ArrowBack, - contentDescription = stringResource(R.string.share_cancel), - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - navigationIconContentColor = MaterialTheme.colorScheme.onSurface, - titleContentColor = MaterialTheme.colorScheme.onSurface, - ), - ) -} diff --git a/src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt b/src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt deleted file mode 100644 index 8ed5fc38d..000000000 --- a/src/com/android/messaging/ui/shareintent/screen/ShareIntentEffectHandler.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.messaging.ui.shareintent.screen - -import android.app.Activity -import com.android.messaging.ui.shareintent.screen.model.ShareIntentScreenEffect as Effect - -internal interface ShareIntentEffectHandler { - fun handle(effect: Effect) -} - -internal class ShareIntentEffectHandlerImpl( - private val activity: Activity, -) : ShareIntentEffectHandler { - - override fun handle(effect: Effect) { - } -} diff --git a/src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt b/src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt deleted file mode 100644 index c19655513..000000000 --- a/src/com/android/messaging/ui/shareintent/screen/ShareIntentScreen.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.android.messaging.ui.shareintent.screen - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import com.android.messaging.ui.shareintent.common.ShareIntentTopAppBar -import com.android.messaging.ui.shareintent.common.contentSurfaceShape -import com.android.messaging.ui.shareintent.screen.model.ShareIntentAction as Action -import com.android.messaging.ui.shareintent.screen.model.ShareIntentUiState as State - -@Composable -internal fun ShareIntentScreen( - effectHandler: ShareIntentEffectHandler, - onNavigateBack: () -> Unit, - modifier: Modifier = Modifier, - screenModel: ShareIntentScreenModel = viewModel(), -) { - val uiState by screenModel.uiState.collectAsStateWithLifecycle() - - val currentEffectHandler by rememberUpdatedState(effectHandler) - LaunchedEffect(screenModel) { - screenModel.effects.collect { effect -> - currentEffectHandler.handle(effect) - } - } - - ShareIntentContent( - uiState = uiState, - onAction = screenModel::onAction, - onNavigateBack = onNavigateBack, - modifier = modifier, - ) -} - -@Composable -private fun ShareIntentContent( - uiState: State, - onAction: (Action) -> Unit, - onNavigateBack: () -> Unit, - modifier: Modifier = Modifier, -) { - Scaffold( - modifier = modifier, - containerColor = MaterialTheme.colorScheme.surfaceContainer, - topBar = { - ShareIntentTopAppBar(onClose = onNavigateBack) - }, - ) { contentPadding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = contentPadding.calculateTopPadding()) - .clip(MaterialTheme.contentSurfaceShape) - .background(MaterialTheme.colorScheme.background), - ) - } -} diff --git a/src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt b/src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt deleted file mode 100644 index 54806575c..000000000 --- a/src/com/android/messaging/ui/shareintent/screen/ShareIntentViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.android.messaging.ui.shareintent.screen - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.android.messaging.ui.shareintent.screen.delegate.ShareIntentScreenDelegate -import com.android.messaging.ui.shareintent.screen.model.ShareIntentAction as Action -import com.android.messaging.ui.shareintent.screen.model.ShareIntentScreenEffect as Effect -import com.android.messaging.ui.shareintent.screen.model.ShareIntentUiState as State -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow - -internal interface ShareIntentScreenModel { - val effects: Flow - val uiState: StateFlow - - fun onAction(action: Action) -} - -@HiltViewModel -internal class ShareIntentViewModel @Inject constructor( - delegate: ShareIntentScreenDelegate, -) : ViewModel(), - ShareIntentScreenModel { - - private val _effects = MutableSharedFlow(extraBufferCapacity = 1) - override val effects: Flow = _effects.asSharedFlow() - - override val uiState: StateFlow = delegate.state - - init { - delegate.bind(viewModelScope) - } - - override fun onAction(action: Action) { - } -} diff --git a/src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt b/src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt deleted file mode 100644 index 0592007dd..000000000 --- a/src/com/android/messaging/ui/shareintent/screen/delegate/ShareIntentDelegate.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.messaging.ui.shareintent.screen.delegate - -import com.android.messaging.di.core.DefaultDispatcher -import com.android.messaging.ui.shareintent.screen.model.ShareIntentUiState as State -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -internal interface ShareIntentScreenDelegate { - val state: StateFlow - fun bind(scope: CoroutineScope) -} - -internal class ShareIntentScreenDelegateImpl @Inject constructor( - @param:DefaultDispatcher - private val defaultDispatcher: CoroutineDispatcher, -) : ShareIntentScreenDelegate { - - private val _state = MutableStateFlow(State()) - override val state: StateFlow = _state.asStateFlow() - - private var isBound = false - - override fun bind(scope: CoroutineScope) { - if (isBound) return - isBound = true - - scope.launch(defaultDispatcher) { - } - } -} diff --git a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt deleted file mode 100644 index 3f13dc46c..000000000 --- a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentAction.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.android.messaging.ui.shareintent.screen.model - -internal sealed interface ShareIntentAction diff --git a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt deleted file mode 100644 index efd4ffb84..000000000 --- a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentScreenEffect.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.android.messaging.ui.shareintent.screen.model - -internal sealed interface ShareIntentScreenEffect diff --git a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt b/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt deleted file mode 100644 index 55a90d796..000000000 --- a/src/com/android/messaging/ui/shareintent/screen/model/ShareIntentUiState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.messaging.ui.shareintent.screen.model - -import androidx.compose.runtime.Immutable -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Immutable -internal data class ShareIntentUiState( - val isLoading: Boolean = true, - val targets: ImmutableList = persistentListOf(), -) - -@Immutable -internal data class ShareTargetUiState( - val conversationId: String, -) From c1064311c72a1b0455dc6a3e2f36ee63b88d8cb7 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Thu, 25 Jun 2026 11:59:26 +0200 Subject: [PATCH 8/9] Add caption for forward message attachments --- ...ldConversationDraftFromMessageImplTest.kt} | 46 +++++++++++++++---- .../ConversationPickerBindsModule.kt | 8 ++++ .../di/forward/ForwardBindsModule.kt | 20 -------- .../BuildConversationDraftFromMessage.kt} | 9 ++-- .../host/forward/ForwardMessageActivity.kt | 6 +-- 5 files changed, 53 insertions(+), 36 deletions(-) rename app/src/test/kotlin/com/android/messaging/domain/{forward/usecase/BuildForwardConversationDraftImplTest.kt => conversationpicker/usecase/BuildConversationDraftFromMessageImplTest.kt} (71%) delete mode 100644 src/com/android/messaging/di/forward/ForwardBindsModule.kt rename src/com/android/messaging/domain/{forward/usecase/BuildForwardConversationDraft.kt => conversationpicker/usecase/BuildConversationDraftFromMessage.kt} (79%) diff --git a/app/src/test/kotlin/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraftImplTest.kt b/app/src/test/kotlin/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessageImplTest.kt similarity index 71% rename from app/src/test/kotlin/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraftImplTest.kt rename to app/src/test/kotlin/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessageImplTest.kt index 846a2139c..c48028afe 100644 --- a/app/src/test/kotlin/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraftImplTest.kt +++ b/app/src/test/kotlin/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessageImplTest.kt @@ -1,4 +1,4 @@ -package com.android.messaging.domain.forward.usecase +package com.android.messaging.domain.conversationpicker.usecase import android.net.Uri import com.android.messaging.data.conversation.model.draft.ConversationDraftAttachment @@ -12,9 +12,9 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -internal class BuildForwardConversationDraftImplTest { +internal class BuildConversationDraftFromMessageImplTest { - private val buildForwardConversationDraft = BuildForwardConversationDraftImpl() + private val buildConversationDraftFromMessage = BuildConversationDraftFromMessageImpl() @Test fun invoke_textSubjectAndMediaPart_mapsAllFields() { @@ -25,11 +25,12 @@ internal class BuildForwardConversationDraftImplTest { mediaPart( contentType = "image/jpeg", uri = "content://media/1", + caption = "Caption", ), ), ) - val draft = buildForwardConversationDraft(message) + val draft = buildConversationDraftFromMessage(message) assertEquals("Forwarded body", draft.messageText) assertEquals("Forwarded subject", draft.subjectText) @@ -38,12 +39,32 @@ internal class BuildForwardConversationDraftImplTest { ConversationDraftAttachment( contentType = "image/jpeg", contentUri = "content://media/1", + captionText = "Caption", ), ), draft.attachments, ) } + @Test + fun invoke_mediaPartWithoutCaption_mapsToEmptyCaption() { + val message = messageData( + text = "Body", + subject = "", + parts = listOf( + mediaPart( + contentType = "image/png", + uri = "content://media/1", + caption = null, + ), + ), + ) + + val draft = buildConversationDraftFromMessage(message) + + assertEquals("", draft.attachments.single().captionText) + } + @Test fun invoke_nullSubject_mapsToEmptyString() { val message = messageData( @@ -52,7 +73,7 @@ internal class BuildForwardConversationDraftImplTest { parts = emptyList(), ) - val draft = buildForwardConversationDraft(message) + val draft = buildConversationDraftFromMessage(message) assertEquals("", draft.subjectText) } @@ -66,15 +87,17 @@ internal class BuildForwardConversationDraftImplTest { mediaPart( contentType = "text/plain", uri = "content://text/1", + caption = null, ), mediaPart( contentType = "image/png", uri = "content://media/2", + caption = null, ), ), ) - val draft = buildForwardConversationDraft(message) + val draft = buildConversationDraftFromMessage(message) assertEquals( listOf( @@ -100,7 +123,7 @@ internal class BuildForwardConversationDraftImplTest { parts = listOf(partWithoutUri), ) - val draft = buildForwardConversationDraft(message) + val draft = buildConversationDraftFromMessage(message) assertEquals(emptyList(), draft.attachments) } @@ -113,7 +136,7 @@ internal class BuildForwardConversationDraftImplTest { parts = emptyList(), ) - val draft = buildForwardConversationDraft(message) + val draft = buildConversationDraftFromMessage(message) assertEquals(emptyList(), draft.attachments) } @@ -130,10 +153,15 @@ internal class BuildForwardConversationDraftImplTest { } } - private fun mediaPart(contentType: String, uri: String): MessagePartData { + private fun mediaPart( + contentType: String, + uri: String, + caption: String?, + ): MessagePartData { return mockk { every { this@mockk.contentType } returns contentType every { contentUri } returns Uri.parse(uri) + every { text } returns caption } } } diff --git a/src/com/android/messaging/di/conversationpicker/ConversationPickerBindsModule.kt b/src/com/android/messaging/di/conversationpicker/ConversationPickerBindsModule.kt index 19027a6b5..252d83598 100644 --- a/src/com/android/messaging/di/conversationpicker/ConversationPickerBindsModule.kt +++ b/src/com/android/messaging/di/conversationpicker/ConversationPickerBindsModule.kt @@ -2,6 +2,8 @@ package com.android.messaging.di.conversationpicker import com.android.messaging.data.conversationpicker.repository.TargetsRepository import com.android.messaging.data.conversationpicker.repository.TargetsRepositoryImpl +import com.android.messaging.domain.conversationpicker.usecase.BuildConversationDraftFromMessage +import com.android.messaging.domain.conversationpicker.usecase.BuildConversationDraftFromMessageImpl import com.android.messaging.domain.conversationpicker.usecase.BuildMessageDataFromDraft import com.android.messaging.domain.conversationpicker.usecase.BuildMessageDataFromDraftImpl import com.android.messaging.domain.conversationpicker.usecase.ResolveTargetsToConversationIds @@ -42,6 +44,12 @@ internal abstract class ConversationPickerBindsModule { impl: TargetsRepositoryImpl, ): TargetsRepository + @Binds + @Reusable + abstract fun bindBuildConversationDraftFromMessage( + impl: BuildConversationDraftFromMessageImpl, + ): BuildConversationDraftFromMessage + @Binds @Reusable abstract fun bindBuildMessageDataFromDraft( diff --git a/src/com/android/messaging/di/forward/ForwardBindsModule.kt b/src/com/android/messaging/di/forward/ForwardBindsModule.kt deleted file mode 100644 index 879cda486..000000000 --- a/src/com/android/messaging/di/forward/ForwardBindsModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.android.messaging.di.forward - -import com.android.messaging.domain.forward.usecase.BuildForwardConversationDraft -import com.android.messaging.domain.forward.usecase.BuildForwardConversationDraftImpl -import dagger.Binds -import dagger.Module -import dagger.Reusable -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal abstract class ForwardBindsModule { - - @Binds - @Reusable - abstract fun bindBuildForwardConversationDraft( - impl: BuildForwardConversationDraftImpl, - ): BuildForwardConversationDraft -} diff --git a/src/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraft.kt b/src/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessage.kt similarity index 79% rename from src/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraft.kt rename to src/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessage.kt index a0b6742a6..45c0aea96 100644 --- a/src/com/android/messaging/domain/forward/usecase/BuildForwardConversationDraft.kt +++ b/src/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessage.kt @@ -1,4 +1,4 @@ -package com.android.messaging.domain.forward.usecase +package com.android.messaging.domain.conversationpicker.usecase import com.android.messaging.data.conversation.model.draft.ConversationDraft import com.android.messaging.data.conversation.model.draft.ConversationDraftAttachment @@ -7,12 +7,12 @@ import com.android.messaging.util.ContentType import javax.inject.Inject import kotlinx.collections.immutable.toImmutableList -internal interface BuildForwardConversationDraft { +internal interface BuildConversationDraftFromMessage { operator fun invoke(message: MessageData): ConversationDraft } -internal class BuildForwardConversationDraftImpl @Inject constructor() : - BuildForwardConversationDraft { +internal class BuildConversationDraftFromMessageImpl @Inject constructor() : + BuildConversationDraftFromMessage { override fun invoke(message: MessageData): ConversationDraft { val attachments = message.parts @@ -23,6 +23,7 @@ internal class BuildForwardConversationDraftImpl @Inject constructor() : ConversationDraftAttachment( contentType = part.contentType, contentUri = part.contentUri.toString(), + captionText = part.text.orEmpty(), ) } .toImmutableList() diff --git a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt index 06e6d8f34..ded7330df 100644 --- a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt +++ b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt @@ -9,8 +9,8 @@ import androidx.core.content.IntentCompat import com.android.messaging.datamodel.data.MessageData import com.android.messaging.di.core.ApplicationCoroutineScope import com.android.messaging.di.core.MainDispatcher +import com.android.messaging.domain.conversationpicker.usecase.BuildConversationDraftFromMessage import com.android.messaging.domain.conversationpicker.usecase.SendContentToTargets -import com.android.messaging.domain.forward.usecase.BuildForwardConversationDraft import com.android.messaging.ui.UIIntents import com.android.messaging.ui.conversationpicker.ConversationPickerScreen import com.android.messaging.ui.core.AppTheme @@ -34,7 +34,7 @@ class ForwardMessageActivity : ComponentActivity() { internal lateinit var sendContentToTargets: SendContentToTargets @Inject - internal lateinit var buildForwardConversationDraft: BuildForwardConversationDraft + internal lateinit var buildConversationDraftFromMessage: BuildConversationDraftFromMessage override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -55,7 +55,7 @@ class ForwardMessageActivity : ComponentActivity() { setContent { AppTheme { val draft = remember(message) { - buildForwardConversationDraft(message) + buildConversationDraftFromMessage(message) } val effectHandler = remember(message) { From 0c67a418eb240aa1e53cc08466a56b7f4a64aafc Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Fri, 26 Jun 2026 12:22:09 +0200 Subject: [PATCH 9/9] Customize conversation picker labels per host --- .../common/PickerTopAppBarTest.kt | 2 ++ res/values/strings.xml | 4 +++ .../component/RecipientSelectionContent.kt | 2 ++ .../ConversationPickerScreen.kt | 26 ++++++++++++++ .../RecentTargetsSection.kt | 4 ++- .../common/PickerTopAppBar.kt | 20 ++++++++--- .../host/forward/ForwardMessageActivity.kt | 2 ++ .../host/share/ShareIntentActivity.kt | 2 ++ .../widget/WidgetPickConversationActivity.kt | 6 ++-- .../model/ConversationPickerLabels.kt | 34 +++++++++++++++++++ .../RecipientSelectionContactsContent.kt | 10 ++++-- ...tSelectionContactsContentPreviewSupport.kt | 2 ++ 12 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 src/com/android/messaging/ui/conversationpicker/model/ConversationPickerLabels.kt diff --git a/app/src/androidTest/java/com/android/messaging/ui/conversationpicker/common/PickerTopAppBarTest.kt b/app/src/androidTest/java/com/android/messaging/ui/conversationpicker/common/PickerTopAppBarTest.kt index 2fa3ed6de..887fc6d23 100644 --- a/app/src/androidTest/java/com/android/messaging/ui/conversationpicker/common/PickerTopAppBarTest.kt +++ b/app/src/androidTest/java/com/android/messaging/ui/conversationpicker/common/PickerTopAppBarTest.kt @@ -64,6 +64,8 @@ internal class PickerTopAppBarTest { inSelectionMode = inSelectionMode, selectedCount = 1, searchState = TextFieldState(initialText = searchText), + title = R.string.share_intent_activity_label, + searchHint = R.string.share_search_hint, onNavigateBack = {}, onSearchOpen = {}, onSearchClose = {}, diff --git a/res/values/strings.xml b/res/values/strings.xml index 3637c933d..04b84cb68 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -694,6 +694,8 @@ Notes Forward message + + Enter a contact name or phone number to forward to Reply @@ -1074,6 +1076,8 @@ New message Conversation list + + Enter a contact name or phone number to pick a conversation Loading conversations diff --git a/src/com/android/messaging/ui/conversation/recipientpicker/component/RecipientSelectionContent.kt b/src/com/android/messaging/ui/conversation/recipientpicker/component/RecipientSelectionContent.kt index df9318fe1..7bb20eae5 100644 --- a/src/com/android/messaging/ui/conversation/recipientpicker/component/RecipientSelectionContent.kt +++ b/src/com/android/messaging/ui/conversation/recipientpicker/component/RecipientSelectionContent.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +import com.android.messaging.R import com.android.messaging.ui.conversation.preview.previewSimSelectorUiState import com.android.messaging.ui.conversation.recipientpicker.component.simselector.NewChatSimSelectorRow import com.android.messaging.ui.core.MessagingPreviewTheme @@ -232,6 +233,7 @@ private fun RecipientSelectionArmedContactsArea( onRecipientDestinationClick = onRecipientDestinationClickWrapped, onRecipientDestinationLongClick = onRecipientDestinationLongClickWrapped .takeIf { onRecipientDestinationLongClick != null }, + emptyStateText = R.string.contact_list_empty_text, topListContent = topListContent, ) } diff --git a/src/com/android/messaging/ui/conversationpicker/ConversationPickerScreen.kt b/src/com/android/messaging/ui/conversationpicker/ConversationPickerScreen.kt index 39d0f7fe6..b82202718 100644 --- a/src/com/android/messaging/ui/conversationpicker/ConversationPickerScreen.kt +++ b/src/com/android/messaging/ui/conversationpicker/ConversationPickerScreen.kt @@ -65,6 +65,7 @@ import com.android.messaging.ui.conversationpicker.common.SelectedTargetsBar import com.android.messaging.ui.conversationpicker.common.composeSubjectSlot import com.android.messaging.ui.conversationpicker.common.contentSurfaceShape import com.android.messaging.ui.conversationpicker.model.ConversationPickerAction as Action +import com.android.messaging.ui.conversationpicker.model.ConversationPickerLabels import com.android.messaging.ui.conversationpicker.model.ConversationPickerUiState as State import com.android.messaging.ui.conversationpicker.model.DraftUiState import com.android.messaging.ui.conversationpicker.model.RecentTargetsUiState @@ -87,6 +88,7 @@ internal fun ConversationPickerScreen( effectHandler: ConversationPickerEffectHandler, onNavigateBack: () -> Unit, allowMultiSelect: Boolean, + labels: ConversationPickerLabels, modifier: Modifier = Modifier, screenModel: ConversationPickerScreenModel = viewModel(), ) { @@ -128,6 +130,7 @@ internal fun ConversationPickerScreen( permissionLauncher.launch(Manifest.permission.READ_CONTACTS) }, allowMultiSelect = allowMultiSelect, + labels = labels, modifier = modifier, ) } @@ -146,6 +149,7 @@ private fun PickerContent( onNavigateBack: () -> Unit, onGrantContactsPermission: () -> Unit, allowMultiSelect: Boolean, + labels: ConversationPickerLabels, modifier: Modifier = Modifier, ) { val searchState = rememberTextFieldState() @@ -167,6 +171,7 @@ private fun PickerContent( PickerReviewScaffold( uiState = uiState, onAction = onAction, + labels = labels, modifier = modifier, ) } else { @@ -177,6 +182,7 @@ private fun PickerContent( onNavigateBack = onNavigateBack, onGrantContactsPermission = onGrantContactsPermission, allowMultiSelect = allowMultiSelect, + labels = labels, modifier = modifier, ) } @@ -212,6 +218,7 @@ private fun PickerScaffold( onNavigateBack: () -> Unit, onGrantContactsPermission: () -> Unit, allowMultiSelect: Boolean, + labels: ConversationPickerLabels, modifier: Modifier = Modifier, ) { val inSelectionMode = uiState.targets.selection.selectedIds.isNotEmpty() @@ -226,6 +233,7 @@ private fun PickerScaffold( inSelectionMode = inSelectionMode, onAction = onAction, onNavigateBack = onNavigateBack, + labels = labels, ) }, ) { contentPadding -> @@ -256,6 +264,7 @@ private fun PickerScaffold( onAction = onAction, onGrantContactsPermission = onGrantContactsPermission, bottomPadding = contentPadding.calculateBottomPadding(), + labels = labels, ) } } @@ -272,6 +281,7 @@ private fun PickerTargetsContent( onAction: (Action) -> Unit, onGrantContactsPermission: () -> Unit, bottomPadding: Dp, + labels: ConversationPickerLabels, modifier: Modifier = Modifier, ) { if (!uiState.contacts.hasContactsPermission) { @@ -282,6 +292,7 @@ private fun PickerTargetsContent( onAction = onAction, onGrantContactsPermission = onGrantContactsPermission, bottomPadding = bottomPadding, + labels = labels, modifier = modifier, ) return @@ -294,6 +305,7 @@ private fun PickerTargetsContent( onAction = onAction, onGrantContactsPermission = onGrantContactsPermission, bottomPadding = bottomPadding, + labels = labels, modifier = modifier, ) } @@ -306,6 +318,7 @@ private fun PickerRecentTargetsContent( onAction: (Action) -> Unit, onGrantContactsPermission: () -> Unit, bottomPadding: Dp, + labels: ConversationPickerLabels, modifier: Modifier = Modifier, ) { SelectionListContent( @@ -333,6 +346,7 @@ private fun PickerRecentTargetsContent( hasContactsPermission = uiState.contacts.hasContactsPermission, onAction = onAction, onGrantContactsPermission = onGrantContactsPermission, + recentConversationsTitle = labels.recentConversationsTitle, modifier = Modifier.animateItem(), ) } @@ -347,6 +361,7 @@ private fun PickerContactsTargetsContent( onAction: (Action) -> Unit, onGrantContactsPermission: () -> Unit, bottomPadding: Dp, + labels: ConversationPickerLabels, modifier: Modifier = Modifier, ) { RecipientSelectionContactsContent( @@ -358,6 +373,7 @@ private fun PickerContactsTargetsContent( bottom = bottomPadding, ), uiState = uiState.asRecipientSelectionState(), + emptyStateText = labels.emptyStateText, rowDecorators = pickerContactRowDecorators, onLoadMore = { onAction(Action.LoadMoreContacts) }, onPrimaryActionClick = {}, @@ -393,6 +409,7 @@ private fun PickerContactsTargetsContent( hasContactsPermission = uiState.contacts.hasContactsPermission, onAction = onAction, onGrantContactsPermission = onGrantContactsPermission, + recentConversationsTitle = labels.recentConversationsTitle, ) } } @@ -407,6 +424,7 @@ private fun PickerTopBar( inSelectionMode: Boolean, onAction: (Action) -> Unit, onNavigateBack: () -> Unit, + labels: ConversationPickerLabels, ) { Column( modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer), @@ -416,6 +434,8 @@ private fun PickerTopBar( inSelectionMode = inSelectionMode, selectedCount = uiState.targets.selection.selectedIds.size, searchState = searchState, + title = labels.title, + searchHint = labels.searchHint, onNavigateBack = onNavigateBack, onSearchOpen = { onAction(Action.SearchOpened) }, onSearchClose = { @@ -444,6 +464,7 @@ private fun PickerTopBar( private fun PickerReviewScaffold( uiState: State, onAction: (Action) -> Unit, + labels: ConversationPickerLabels, modifier: Modifier = Modifier, ) { Scaffold( @@ -454,6 +475,7 @@ private fun PickerReviewScaffold( modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer), ) { PickerReviewTopAppBar( + title = labels.title, onBack = { onAction(Action.ReviewDismissed) }, ) @@ -625,6 +647,7 @@ private fun PickerContentPreview() { onNavigateBack = {}, onGrantContactsPermission = {}, allowMultiSelect = true, + labels = ConversationPickerLabels.Share, ) } } @@ -671,6 +694,7 @@ private fun PickerSelectionPreview() { onNavigateBack = {}, onGrantContactsPermission = {}, allowMultiSelect = true, + labels = ConversationPickerLabels.Share, ) } } @@ -688,6 +712,7 @@ private fun PickerEmptyPreview() { onNavigateBack = {}, onGrantContactsPermission = {}, allowMultiSelect = true, + labels = ConversationPickerLabels.Share, ) } } @@ -706,6 +731,7 @@ private fun PickerContactsPermissionPreview() { onNavigateBack = {}, onGrantContactsPermission = {}, allowMultiSelect = true, + labels = ConversationPickerLabels.Share, ) } } diff --git a/src/com/android/messaging/ui/conversationpicker/RecentTargetsSection.kt b/src/com/android/messaging/ui/conversationpicker/RecentTargetsSection.kt index 694852e9d..39be8bf5c 100644 --- a/src/com/android/messaging/ui/conversationpicker/RecentTargetsSection.kt +++ b/src/com/android/messaging/ui/conversationpicker/RecentTargetsSection.kt @@ -1,5 +1,6 @@ package com.android.messaging.ui.conversationpicker +import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -32,12 +33,13 @@ internal fun RecentTargetsSection( hasContactsPermission: Boolean, onAction: (Action) -> Unit, onGrantContactsPermission: () -> Unit, + @StringRes recentConversationsTitle: Int, modifier: Modifier = Modifier, ) { Column(modifier = modifier.fillMaxWidth()) { if (recentTargets.isNotEmpty()) { SectionHeader( - text = stringResource(R.string.share_recent_conversations_title), + text = stringResource(id = recentConversationsTitle), ) } diff --git a/src/com/android/messaging/ui/conversationpicker/common/PickerTopAppBar.kt b/src/com/android/messaging/ui/conversationpicker/common/PickerTopAppBar.kt index 8a23c8150..21f3e0c2c 100644 --- a/src/com/android/messaging/ui/conversationpicker/common/PickerTopAppBar.kt +++ b/src/com/android/messaging/ui/conversationpicker/common/PickerTopAppBar.kt @@ -1,5 +1,6 @@ package com.android.messaging.ui.conversationpicker.common +import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.BasicTextField @@ -37,6 +38,8 @@ internal fun PickerTopAppBar( inSelectionMode: Boolean, selectedCount: Int, searchState: TextFieldState, + @StringRes title: Int, + @StringRes searchHint: Int, onNavigateBack: () -> Unit, onSearchOpen: () -> Unit, onSearchClose: () -> Unit, @@ -49,6 +52,8 @@ internal fun PickerTopAppBar( inSelectionMode = inSelectionMode, selectedCount = selectedCount, searchState = searchState, + title = title, + searchHint = searchHint, ) }, navigationIcon = { @@ -80,12 +85,13 @@ internal fun PickerTopAppBar( @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun PickerReviewTopAppBar( + @StringRes title: Int, onBack: () -> Unit, ) { TopAppBar( title = { Text( - text = stringResource(R.string.share_intent_activity_label), + text = stringResource(id = title), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, @@ -114,10 +120,15 @@ private fun PickerTopAppBarTitle( inSelectionMode: Boolean, selectedCount: Int, searchState: TextFieldState, + @StringRes title: Int, + @StringRes searchHint: Int, ) { when { isSearchActive -> { - PickerSearchField(state = searchState) + PickerSearchField( + state = searchState, + searchHint = searchHint, + ) } inSelectionMode -> { @@ -132,7 +143,7 @@ private fun PickerTopAppBarTitle( else -> { Text( - text = stringResource(R.string.share_intent_activity_label), + text = stringResource(id = title), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, @@ -209,6 +220,7 @@ private fun PickerTopAppBarActions( @Composable private fun PickerSearchField( state: TextFieldState, + @StringRes searchHint: Int, ) { val focusRequester = remember { FocusRequester() } @@ -231,7 +243,7 @@ private fun PickerSearchField( Box { if (state.text.isEmpty()) { Text( - text = stringResource(R.string.share_search_hint), + text = stringResource(id = searchHint), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, diff --git a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt index ded7330df..ffa5823a9 100644 --- a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt +++ b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt @@ -13,6 +13,7 @@ import com.android.messaging.domain.conversationpicker.usecase.BuildConversation import com.android.messaging.domain.conversationpicker.usecase.SendContentToTargets import com.android.messaging.ui.UIIntents import com.android.messaging.ui.conversationpicker.ConversationPickerScreen +import com.android.messaging.ui.conversationpicker.model.ConversationPickerLabels import com.android.messaging.ui.core.AppTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -72,6 +73,7 @@ class ForwardMessageActivity : ComponentActivity() { effectHandler = effectHandler, onNavigateBack = ::finish, allowMultiSelect = true, + labels = ConversationPickerLabels.Forward, isInitialDraftLoading = false, initialDraft = draft, ) diff --git a/src/com/android/messaging/ui/conversationpicker/host/share/ShareIntentActivity.kt b/src/com/android/messaging/ui/conversationpicker/host/share/ShareIntentActivity.kt index 08674f8d5..52029c360 100644 --- a/src/com/android/messaging/ui/conversationpicker/host/share/ShareIntentActivity.kt +++ b/src/com/android/messaging/ui/conversationpicker/host/share/ShareIntentActivity.kt @@ -19,6 +19,7 @@ import com.android.messaging.domain.shareintent.model.SharedConversationDraftRes import com.android.messaging.domain.shareintent.usecase.BuildSharedConversationDraft import com.android.messaging.ui.UIIntents import com.android.messaging.ui.conversationpicker.ConversationPickerScreen +import com.android.messaging.ui.conversationpicker.model.ConversationPickerLabels import com.android.messaging.ui.core.AppTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -80,6 +81,7 @@ class ShareIntentActivity : ComponentActivity() { effectHandler = effectHandler, onNavigateBack = ::finish, allowMultiSelect = true, + labels = ConversationPickerLabels.Share, isInitialDraftLoading = shareDraft.isLoading, initialDraft = shareDraft.draft, ) diff --git a/src/com/android/messaging/ui/conversationpicker/host/widget/WidgetPickConversationActivity.kt b/src/com/android/messaging/ui/conversationpicker/host/widget/WidgetPickConversationActivity.kt index 3aab3938c..0c5d3c393 100644 --- a/src/com/android/messaging/ui/conversationpicker/host/widget/WidgetPickConversationActivity.kt +++ b/src/com/android/messaging/ui/conversationpicker/host/widget/WidgetPickConversationActivity.kt @@ -6,6 +6,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.android.messaging.ui.conversationpicker.ConversationPickerScreen +import com.android.messaging.ui.conversationpicker.model.ConversationPickerLabels import com.android.messaging.ui.core.AppTheme import dagger.hilt.android.AndroidEntryPoint @@ -35,11 +36,12 @@ class WidgetPickConversationActivity : ComponentActivity() { setContent { AppTheme { ConversationPickerScreen( + effectHandler = effectHandler, + onNavigateBack = ::finish, allowMultiSelect = false, + labels = ConversationPickerLabels.Widget, isInitialDraftLoading = false, initialDraft = null, - effectHandler = effectHandler, - onNavigateBack = ::finish, ) } } diff --git a/src/com/android/messaging/ui/conversationpicker/model/ConversationPickerLabels.kt b/src/com/android/messaging/ui/conversationpicker/model/ConversationPickerLabels.kt new file mode 100644 index 000000000..42975e0f0 --- /dev/null +++ b/src/com/android/messaging/ui/conversationpicker/model/ConversationPickerLabels.kt @@ -0,0 +1,34 @@ +package com.android.messaging.ui.conversationpicker.model + +import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable +import com.android.messaging.R + +@Immutable +sealed class ConversationPickerLabels { + + @get:StringRes + open val title: Int = R.string.share_intent_activity_label + + @get:StringRes + open val recentConversationsTitle: Int = R.string.share_recent_conversations_title + + @get:StringRes + open val searchHint: Int = R.string.share_search_hint + + @get:StringRes + abstract val emptyStateText: Int + + data object Share : ConversationPickerLabels() { + override val emptyStateText = R.string.contact_list_empty_text + } + + data object Forward : ConversationPickerLabels() { + override val title = R.string.forward_message_activity_title + override val emptyStateText = R.string.forward_picker_empty_text + } + + data object Widget : ConversationPickerLabels() { + override val emptyStateText = R.string.widget_picker_empty_text + } +} diff --git a/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContent.kt b/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContent.kt index dd0fbfc3c..f2fed1886 100644 --- a/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContent.kt +++ b/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContent.kt @@ -1,5 +1,6 @@ package com.android.messaging.ui.recipientselection.component +import androidx.annotation.StringRes import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.Spring @@ -31,7 +32,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp -import com.android.messaging.R import com.android.messaging.ui.common.components.selection.SelectionListContent import com.android.messaging.ui.core.MessagingPreviewColumn import com.android.messaging.ui.recipientselection.model.section.RecipientContactListEntry @@ -53,6 +53,7 @@ internal fun RecipientSelectionContactsContent( onPrimaryActionClick: () -> Unit, onRecipientDestinationClick: OnRecipientDestinationAction, onRecipientDestinationLongClick: OnRecipientDestinationAction?, + @StringRes emptyStateText: Int, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), topListContent: (@Composable () -> Unit)? = null, @@ -111,6 +112,7 @@ internal fun RecipientSelectionContactsContent( rowDecorators = rowDecorators, onRecipientDestinationClick = onRecipientDestinationClick, onRecipientDestinationLongClick = onRecipientDestinationLongClick, + emptyStateText = emptyStateText, ) } } @@ -122,6 +124,7 @@ private fun LazyListScope.recipientSelectionContactItems( rowDecorators: RecipientSelectionRowDecorators, onRecipientDestinationClick: OnRecipientDestinationAction, onRecipientDestinationLongClick: OnRecipientDestinationAction?, + @StringRes emptyStateText: Int, ) { val pickerUiState = uiState.picker @@ -134,7 +137,7 @@ private fun LazyListScope.recipientSelectionContactItems( pickerUiState.items.isEmpty() -> { item { - RecipientSelectionEmptyState() + RecipientSelectionEmptyState(text = emptyStateText) } } @@ -302,13 +305,14 @@ private fun RecipientSelectionLoadingMoreState() { @Composable private fun RecipientSelectionEmptyState( + @StringRes text: Int, modifier: Modifier = Modifier, ) { Text( modifier = modifier .fillMaxWidth() .padding(all = 24.dp), - text = stringResource(id = R.string.contact_list_empty_text), + text = stringResource(id = text), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, diff --git a/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContentPreviewSupport.kt b/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContentPreviewSupport.kt index 336a82379..de4c6e091 100644 --- a/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContentPreviewSupport.kt +++ b/src/com/android/messaging/ui/recipientselection/component/RecipientSelectionContactsContentPreviewSupport.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.android.messaging.R import com.android.messaging.ui.recipientselection.model.picker.RecipientPickerListItem import com.android.messaging.ui.recipientselection.model.picker.RecipientPickerUiState import com.android.messaging.ui.recipientselection.model.picker.SelectedRecipient @@ -38,6 +39,7 @@ internal fun PreviewRecipientSelectionContactsContent( onPrimaryActionClick = {}, onRecipientDestinationClick = { _, _ -> }, onRecipientDestinationLongClick = onRecipientDestinationLongClick, + emptyStateText = R.string.contact_list_empty_text, topListContent = topListContent, ) }