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,
)
}