From cfaef77b216b33d9191f92875c01b9d00525c8c2 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 29 Jun 2023 10:53:25 -0400 Subject: [PATCH] Properly plumb attachment keyboard in CFv2. --- .../v2/ConversationActivityResultContracts.kt | 166 +++++++++++++++++- .../conversation/v2/ConversationFragment.kt | 40 ++++- .../securesms/maps/PlacePickerActivity.java | 2 +- 3 files changed, 196 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt index f7aeaed120..81628047db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt @@ -5,20 +5,30 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.Manifest +import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri +import android.provider.ContactsContract import androidx.activity.result.contract.ActivityResultContract import androidx.core.content.IntentCompat import androidx.fragment.app.Fragment +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.location.SignalPlace import org.thoughtcrime.securesms.contactshare.Contact import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity import org.thoughtcrime.securesms.conversation.MessageSendType import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityResultContracts.Callbacks import org.thoughtcrime.securesms.giph.ui.GiphyActivity +import org.thoughtcrime.securesms.maps.PlacePickerActivity import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity +import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.RecipientId /** @@ -29,16 +39,44 @@ import org.thoughtcrime.securesms.recipients.RecipientId * Note, not all activity results will live here but this should handle most of the basic cases. More advance * usages like [AddToContactsContract] can be split out into their own [ActivityResultContract] implementations. */ -class ConversationActivityResultContracts(fragment: Fragment, private val callbacks: Callbacks) { +class ConversationActivityResultContracts(private val fragment: Fragment, private val callbacks: Callbacks) { + + companion object { + private val TAG = Log.tag(ConversationActivityResultContracts::class.java) + } private val contactShareLauncher = fragment.registerForActivityResult(ContactShareEditor) { contacts -> callbacks.onSendContacts(contacts) } + private val selectContactLauncher = fragment.registerForActivityResult(SelectContact) { uri -> callbacks.onContactSelect(uri) } private val mediaSelectionLauncher = fragment.registerForActivityResult(MediaSelection) { result -> callbacks.onMediaSend(result) } private val gifSearchLauncher = fragment.registerForActivityResult(GifSearch) { result -> callbacks.onMediaSend(result) } + private val mediaGalleryLauncher = fragment.registerForActivityResult(MediaGallery) { result -> callbacks.onMediaSend(result) } + private val selectLocationLauncher = fragment.registerForActivityResult(SelectLocation) { result -> callbacks.onLocationSelected(result?.place, result?.uri) } + private val selectFileLauncher = fragment.registerForActivityResult(SelectFile) { result -> callbacks.onFileSelected(result) } fun launchContactShareEditor(uri: Uri, chatColors: ChatColors) { contactShareLauncher.launch(uri to chatColors) } + fun launchSelectContact() { + Permissions + .with(fragment) + .request(Manifest.permission.READ_CONTACTS) + .ifNecessary() + .withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information)) + .onAllGranted { selectContactLauncher.launch(Unit) } + .execute() + } + + fun launchGallery(recipientId: RecipientId, text: CharSequence?, isReply: Boolean) { + Permissions + .with(fragment) + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .ifNecessary() + .withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) + .onAllGranted { mediaGalleryLauncher.launch(MediaSelectionInput(emptyList(), recipientId, text, isReply)) } + .execute() + } + fun launchMediaEditor(mediaList: List, recipientId: RecipientId, text: CharSequence?) { mediaSelectionLauncher.launch(MediaSelectionInput(mediaList, recipientId, text)) } @@ -47,6 +85,37 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba gifSearchLauncher.launch(GifSearchInput(recipientId, text)) } + fun launchSelectLocation(chatColors: ChatColors) { + if (Permissions.hasAny(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)) { + selectLocationLauncher.launch(chatColors) + } else { + Permissions.with(fragment) + .request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) + .ifNecessary() + .withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location)) + .onSomeGranted { selectLocationLauncher.launch(chatColors) } + .execute() + } + } + + fun launchSelectFile(): Boolean { + try { + selectFileLauncher.launch(SelectFile.SelectFileMode.DOCUMENT) + return true + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "couldn't complete ACTION_OPEN_DOCUMENT, no activity found. falling back.") + } + + try { + selectFileLauncher.launch(SelectFile.SelectFileMode.CONTENT) + return true + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back.") + } + + return false + } + private object MediaSelection : ActivityResultContract() { override fun createIntent(context: Context, input: MediaSelectionInput): Intent { val (media, recipientId, text) = input @@ -54,7 +123,26 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba } override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { - return intent?.let { MediaSendActivityResult.fromData(intent) } + return if (resultCode == Activity.RESULT_OK) { + intent?.let { MediaSendActivityResult.fromData(intent) } + } else { + null + } + } + } + + private object MediaGallery : ActivityResultContract() { + override fun createIntent(context: Context, input: MediaSelectionInput): Intent { + val (media, recipientId, text, isReply) = input + return MediaSelectionActivity.gallery(context, MessageSendType.SignalMessageSendType, media, recipientId, text, isReply) + } + + override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { + return if (resultCode == Activity.RESULT_OK) { + intent?.let { MediaSendActivityResult.fromData(intent) } + } else { + null + } } } @@ -65,7 +153,25 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba } override fun parseResult(resultCode: Int, intent: Intent?): List { - return intent?.let { IntentCompat.getParcelableArrayListExtra(intent, ContactShareEditActivity.KEY_CONTACTS, Contact::class.java) } ?: emptyList() + return if (resultCode == Activity.RESULT_OK) { + intent?.let { IntentCompat.getParcelableArrayListExtra(intent, ContactShareEditActivity.KEY_CONTACTS, Contact::class.java) } ?: emptyList() + } else { + emptyList() + } + } + } + + private object SelectContact : ActivityResultContract() { + override fun createIntent(context: Context, input: Unit): Intent { + return Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return if (resultCode == Activity.RESULT_OK) { + intent?.data + } else { + null + } } } @@ -80,16 +186,66 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba } override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { - return intent?.let { MediaSendActivityResult.fromData(intent) } + return if (resultCode == Activity.RESULT_OK) { + intent?.let { MediaSendActivityResult.fromData(intent) } + } else { + null + } } } - private data class MediaSelectionInput(val media: List, val recipientId: RecipientId, val text: CharSequence?) + private object SelectLocation : ActivityResultContract() { + override fun createIntent(context: Context, input: ChatColors): Intent { + return Intent(context, PlacePickerActivity::class.java) + .putExtra(PlacePickerActivity.KEY_CHAT_COLOR, input.asSingleColor()) + } + + override fun parseResult(resultCode: Int, intent: Intent?): SelectLocationOutput? { + return if (resultCode == Activity.RESULT_OK) { + intent?.data?.let { uri -> SelectLocationOutput(SignalPlace(PlacePickerActivity.addressFromData(intent)), uri) } + } else { + null + } + } + } + + private object SelectFile : ActivityResultContract() { + override fun createIntent(context: Context, input: SelectFileMode): Intent { + return Intent().apply { + type = "*/*" + + action = when (input) { + SelectFileMode.DOCUMENT -> Intent.ACTION_OPEN_DOCUMENT + SelectFileMode.CONTENT -> Intent.ACTION_GET_CONTENT + } + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return if (resultCode == Activity.RESULT_OK) { + intent?.data + } else { + null + } + } + + enum class SelectFileMode { + DOCUMENT, + CONTENT + } + } + + private data class MediaSelectionInput(val media: List, val recipientId: RecipientId, val text: CharSequence?, val isReply: Boolean = false) private data class GifSearchInput(val recipientId: RecipientId, val text: CharSequence?) + private data class SelectLocationOutput(val place: SignalPlace, val uri: Uri) + interface Callbacks { fun onSendContacts(contacts: List) fun onMediaSend(result: MediaSendActivityResult?) + fun onContactSelect(uri: Uri?) + fun onLocationSelected(place: SignalPlace?, uri: Uri?) + fun onFileSelected(uri: Uri?) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index af386a26e4..2e4dad193d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -113,6 +113,7 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.emoji.EmojiEventListener import org.thoughtcrime.securesms.components.emoji.MediaKeyboard import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel +import org.thoughtcrime.securesms.components.location.SignalPlace import org.thoughtcrime.securesms.components.mention.MentionAnnotation import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar @@ -2723,6 +2724,28 @@ class ConversationFragment : preUploadResults = result.preUploadResults ) } + + override fun onContactSelect(uri: Uri?) { + val recipient = viewModel.recipientSnapshot + if (uri != null && recipient != null) { + conversationActivityResultContracts.launchContactShareEditor(uri, recipient.chatColors) + } + } + + override fun onLocationSelected(place: SignalPlace?, uri: Uri?) { + if (place != null && uri != null) { + attachmentManager.setLocation(place, uri) + draftViewModel.setLocationDraft(place) + } else { + Log.w(TAG, "Location missing thumbnail") + } + } + + override fun onFileSelected(uri: Uri?) { + if (uri != null) { + setMedia(uri, SlideFactory.MediaType.DOCUMENT) + } + } } //endregion @@ -3164,19 +3187,24 @@ class ConversationFragment : private inner class AttachmentKeyboardFragmentListener : FragmentResultListener { @Suppress("DEPRECATION") override fun onFragmentResult(requestKey: String, result: Bundle) { + val recipient = viewModel.recipientSnapshot ?: return val button: AttachmentKeyboardButton? = result.getSerializable(AttachmentKeyboardFragment.BUTTON_RESULT) as? AttachmentKeyboardButton val media: Media? = result.getParcelable(AttachmentKeyboardFragment.MEDIA_RESULT) if (button != null) { when (button) { - AttachmentKeyboardButton.GALLERY -> AttachmentManager.selectGallery(this@ConversationFragment, 1, viewModel.recipientSnapshot!!, composeText.textTrimmed, sendButton.selectedSendType, inputPanel.quote.isPresent) - AttachmentKeyboardButton.FILE -> AttachmentManager.selectDocument(this@ConversationFragment, 1) - AttachmentKeyboardButton.CONTACT -> AttachmentManager.selectContactInfo(this@ConversationFragment, 1) - AttachmentKeyboardButton.LOCATION -> AttachmentManager.selectLocation(this@ConversationFragment, 1, viewModel.recipientSnapshot!!.chatColors.asSingleColor()) - AttachmentKeyboardButton.PAYMENT -> AttachmentManager.selectPayment(this@ConversationFragment, viewModel.recipientSnapshot!!) + AttachmentKeyboardButton.GALLERY -> conversationActivityResultContracts.launchGallery(recipient.id, composeText.textTrimmed, inputPanel.quote.isPresent) + AttachmentKeyboardButton.CONTACT -> conversationActivityResultContracts.launchSelectContact() + AttachmentKeyboardButton.LOCATION -> conversationActivityResultContracts.launchSelectLocation(recipient.chatColors) + AttachmentKeyboardButton.PAYMENT -> AttachmentManager.selectPayment(this@ConversationFragment, recipient) + AttachmentKeyboardButton.FILE -> { + if (!conversationActivityResultContracts.launchSelectFile()) { + toast(R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG) + } + } } } else if (media != null) { - conversationActivityResultContracts.launchMediaEditor(listOf(media), viewModel.recipientSnapshot!!.id, composeText.textTrimmed) + conversationActivityResultContracts.launchMediaEditor(listOf(media), recipient.id, composeText.textTrimmed) } container.hideInput() diff --git a/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java b/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java index a0e70671ff..24884f4a28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/maps/PlacePickerActivity.java @@ -64,7 +64,7 @@ public final class PlacePickerActivity extends AppCompatActivity { private static final int ANIMATION_DURATION = 250; private static final OvershootInterpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator(); - private static final String KEY_CHAT_COLOR = "chat_color"; + public static final String KEY_CHAT_COLOR = "chat_color"; private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();