diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c97ea2eac..f18f514e9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -247,12 +247,12 @@ - + android:theme="@style/Theme.Compose" + android:windowSoftInputMode="stateHidden|adjustResize" /> 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/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/domain/conversationpicker/usecase/BuildConversationDraftFromMessage.kt b/src/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessage.kt new file mode 100644 index 000000000..45c0aea96 --- /dev/null +++ b/src/com/android/messaging/domain/conversationpicker/usecase/BuildConversationDraftFromMessage.kt @@ -0,0 +1,37 @@ +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 +import com.android.messaging.datamodel.data.MessageData +import com.android.messaging.util.ContentType +import javax.inject.Inject +import kotlinx.collections.immutable.toImmutableList + +internal interface BuildConversationDraftFromMessage { + operator fun invoke(message: MessageData): ConversationDraft +} + +internal class BuildConversationDraftFromMessageImpl @Inject constructor() : + BuildConversationDraftFromMessage { + + 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(), + captionText = part.text.orEmpty(), + ) + } + .toImmutableList() + + return ConversationDraft( + messageText = message.messageText, + subjectText = message.mmsSubject.orEmpty(), + attachments = attachments, + ) + } +} diff --git a/src/com/android/messaging/ui/UIIntentsImpl.java b/src/com/android/messaging/ui/UIIntentsImpl.java index 6f47c0b7d..01a39d3c9 100644 --- a/src/com/android/messaging/ui/UIIntentsImpl.java +++ b/src/com/android/messaging/ui/UIIntentsImpl.java @@ -50,7 +50,7 @@ import com.android.messaging.ui.conversation.LaunchConversationActivity; import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity; import com.android.messaging.ui.conversationlist.ConversationListActivity; -import com.android.messaging.ui.conversationlist.ForwardMessageActivity; +import com.android.messaging.ui.conversationpicker.host.forward.ForwardMessageActivity; import com.android.messaging.ui.conversationsettings.ConversationSettingsActivity; import com.android.messaging.ui.debug.DebugMmsConfigActivity; import com.android.messaging.ui.permissioncheck.PermissionCheckActivity; 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/conversationlist/ConversationListFragment.java b/src/com/android/messaging/ui/conversationlist/ConversationListFragment.java index 01022a050..30951df8c 100644 --- a/src/com/android/messaging/ui/conversationlist/ConversationListFragment.java +++ b/src/com/android/messaging/ui/conversationlist/ConversationListFragment.java @@ -69,13 +69,11 @@ public class ConversationListFragment extends Fragment implements ConversationListDataListener, ConversationListItemView.HostInterface { private static final String BUNDLE_ARCHIVED_MODE = "archived_mode"; - private static final String BUNDLE_FORWARD_MESSAGE_MODE = "forward_message_mode"; private static final boolean VERBOSE = false; private MenuItem mShowBlockedMenuItem; private boolean mArchiveMode; private boolean mBlockedAvailable; - private boolean mForwardMessageMode; public interface ConversationListFragmentHost { public void onConversationClick(final ConversationListData listData, @@ -108,10 +106,6 @@ public static ConversationListFragment createArchivedConversationListFragment() return createConversationListFragment(BUNDLE_ARCHIVED_MODE); } - public static ConversationListFragment createForwardMessageConversationListFragment() { - return createConversationListFragment(BUNDLE_FORWARD_MESSAGE_MODE); - } - public static ConversationListFragment createConversationListFragment(String modeKeyName) { final ConversationListFragment fragment = new ConversationListFragment(); final Bundle bundle = new Bundle(); @@ -142,7 +136,6 @@ public void onResume() { public void setScrolledToNewestConversationIfNeeded() { if (!mArchiveMode - && !mForwardMessageMode && isScrolledToFirstConversation() && mHost.hasWindowFocus()) { mListBinding.getData().setScrolledToNewestConversation(true); @@ -253,7 +246,6 @@ public void onAttach(final Activity activity) { final Bundle arguments = getArguments(); if (arguments != null) { mArchiveMode = arguments.getBoolean(BUNDLE_ARCHIVED_MODE, false); - mForwardMessageMode = arguments.getBoolean(BUNDLE_FORWARD_MESSAGE_MODE, false); } mListBinding.bind(DataModel.get().createConversationListData(activity, this, mArchiveMode)); } diff --git a/src/com/android/messaging/ui/conversationlist/ForwardMessageActivity.java b/src/com/android/messaging/ui/conversationlist/ForwardMessageActivity.java deleted file mode 100644 index 1968d1cfd..000000000 --- a/src/com/android/messaging/ui/conversationlist/ForwardMessageActivity.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.messaging.ui.conversationlist; - -import android.os.Bundle; - -import androidx.fragment.app.Fragment; - -import com.android.messaging.datamodel.data.ConversationListData; -import com.android.messaging.datamodel.data.ConversationListItemData; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.ui.BaseBugleActivity; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost; -import com.android.messaging.util.Assert; - -/** - * An activity that lets the user forward a SMS/MMS message by picking from a conversation in the - * conversation list. - */ -public class ForwardMessageActivity extends BaseBugleActivity - implements ConversationListFragmentHost { - private MessageData mDraftMessage; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final ConversationListFragment fragment = - ConversationListFragment.createForwardMessageConversationListFragment(); - getSupportFragmentManager().beginTransaction().add(android.R.id.content, fragment).commit(); - mDraftMessage = getIntent().getParcelableExtra(UIIntents.UI_INTENT_EXTRA_DRAFT_DATA); - } - - @Override - public void onAttachFragment(final Fragment fragment) { - if (!(fragment instanceof ConversationListFragment)) { - return; - } - final ConversationListFragment clf = (ConversationListFragment) fragment; - clf.setHost(this); - } - - @Override - public void onConversationClick(final ConversationListData listData, - final ConversationListItemData conversationListItemData, - final boolean isLongClick, final ConversationListItemView converastionView) { - UIIntents.get().launchConversationActivity( - this, conversationListItemData.getConversationId(), mDraftMessage); - } - - @Override - public void onCreateConversationClick() { - UIIntents.get().launchCreateNewConversationActivity(this, mDraftMessage); - } - - @Override - public boolean isConversationSelected(final String conversationId) { - return false; - } - - @Override - public boolean isSwipeAnimatable() { - return false; - } - - @Override - public boolean isSelectionMode() { - return false; - } -} 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 new file mode 100644 index 000000000..ffa5823a9 --- /dev/null +++ b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageActivity.kt @@ -0,0 +1,83 @@ +package com.android.messaging.ui.conversationpicker.host.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.di.core.MainDispatcher +import com.android.messaging.domain.conversationpicker.usecase.BuildConversationDraftFromMessage +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 +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope + +@AndroidEntryPoint +class ForwardMessageActivity : ComponentActivity() { + + @Inject + @ApplicationCoroutineScope + internal lateinit var applicationScope: CoroutineScope + + @Inject + @MainDispatcher + internal lateinit var mainDispatcher: CoroutineDispatcher + + @Inject + internal lateinit var sendContentToTargets: SendContentToTargets + + @Inject + internal lateinit var buildConversationDraftFromMessage: BuildConversationDraftFromMessage + + 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) { + buildConversationDraftFromMessage(message) + } + + val effectHandler = remember(message) { + ForwardMessageEffectHandler( + applicationScope = applicationScope, + mainDispatcher = mainDispatcher, + activity = this, + message = message, + sendContentToTargets = sendContentToTargets, + ) + } + + ConversationPickerScreen( + effectHandler = effectHandler, + onNavigateBack = ::finish, + allowMultiSelect = true, + labels = ConversationPickerLabels.Forward, + isInitialDraftLoading = false, + initialDraft = draft, + ) + } + } + } +} diff --git a/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageEffectHandler.kt b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageEffectHandler.kt new file mode 100644 index 000000000..1429b7a38 --- /dev/null +++ b/src/com/android/messaging/ui/conversationpicker/host/forward/ForwardMessageEffectHandler.kt @@ -0,0 +1,82 @@ +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 ForwardMessageEffectHandler( + private val applicationScope: CoroutineScope, + private val mainDispatcher: CoroutineDispatcher, + private val activity: Activity, + private val message: MessageData, + private val sendContentToTargets: SendContentToTargets, +) : ConversationPickerEffectHandler { + + override fun handle(effect: Effect) { + when (effect) { + is Effect.OpenConversation -> { + 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 -> { + openPreview(effect.contentUri, effect.contentType) + } + } + } + + private fun openPreview( + contentUri: String, + contentType: String, + ) { + applicationScope.launch(mainDispatcher) { + openAttachmentPreview( + context = activity, + contentUri = contentUri, + contentType = contentType, + ) + } + } + + private fun openConversation(conversationId: String) { + UIIntents.get().launchConversationActivity(activity, conversationId, message) + activity.finish() + } + + private fun sendToSelected( + targets: Set, + draft: ConversationDraft, + ) { + applicationScope.launch { + val result = sendContentToTargets(draft, targets) + if (result is SendContentResult.Failure) { + withContext(mainDispatcher) { + UiUtils.showToastAtBottom(R.string.send_message_failure) + } + } + } + + UIIntents.get().launchConversationListActivity(activity) + activity.finish() + } +} 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, ) }