Properly plumb attachment keyboard in CFv2.

This commit is contained in:
Cody Henthorne
2023-06-29 10:53:25 -04:00
committed by Greyson Parrelli
parent 36fc9aa82a
commit cfaef77b21
3 changed files with 196 additions and 12 deletions

View File

@@ -5,20 +5,30 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.ContactsContract
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.fragment.app.Fragment 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.Contact
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity
import org.thoughtcrime.securesms.conversation.MessageSendType import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.colors.ChatColors 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.giph.ui.GiphyActivity
import org.thoughtcrime.securesms.maps.PlacePickerActivity
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.recipients.RecipientId 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 * 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. * 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 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 mediaSelectionLauncher = fragment.registerForActivityResult(MediaSelection) { result -> callbacks.onMediaSend(result) }
private val gifSearchLauncher = fragment.registerForActivityResult(GifSearch) { 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) { fun launchContactShareEditor(uri: Uri, chatColors: ChatColors) {
contactShareLauncher.launch(uri to 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<Media>, recipientId: RecipientId, text: CharSequence?) { fun launchMediaEditor(mediaList: List<Media>, recipientId: RecipientId, text: CharSequence?) {
mediaSelectionLauncher.launch(MediaSelectionInput(mediaList, recipientId, text)) mediaSelectionLauncher.launch(MediaSelectionInput(mediaList, recipientId, text))
} }
@@ -47,6 +85,37 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba
gifSearchLauncher.launch(GifSearchInput(recipientId, text)) 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<MediaSelectionInput, MediaSendActivityResult?>() { private object MediaSelection : ActivityResultContract<MediaSelectionInput, MediaSendActivityResult?>() {
override fun createIntent(context: Context, input: MediaSelectionInput): Intent { override fun createIntent(context: Context, input: MediaSelectionInput): Intent {
val (media, recipientId, text) = input val (media, recipientId, text) = input
@@ -54,7 +123,26 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba
} }
override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { 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<MediaSelectionInput, MediaSendActivityResult?>() {
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<Contact> { override fun parseResult(resultCode: Int, intent: Intent?): List<Contact> {
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<Unit, Uri?>() {
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? { 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<Media>, val recipientId: RecipientId, val text: CharSequence?) private object SelectLocation : ActivityResultContract<ChatColors, SelectLocationOutput?>() {
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<SelectFile.SelectFileMode, Uri?>() {
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<Media>, val recipientId: RecipientId, val text: CharSequence?, val isReply: Boolean = false)
private data class GifSearchInput(val recipientId: RecipientId, val text: CharSequence?) private data class GifSearchInput(val recipientId: RecipientId, val text: CharSequence?)
private data class SelectLocationOutput(val place: SignalPlace, val uri: Uri)
interface Callbacks { interface Callbacks {
fun onSendContacts(contacts: List<Contact>) fun onSendContacts(contacts: List<Contact>)
fun onMediaSend(result: MediaSendActivityResult?) fun onMediaSend(result: MediaSendActivityResult?)
fun onContactSelect(uri: Uri?)
fun onLocationSelected(place: SignalPlace?, uri: Uri?)
fun onFileSelected(uri: Uri?)
} }
} }

View File

@@ -113,6 +113,7 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel 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.mention.MentionAnnotation
import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
@@ -2723,6 +2724,28 @@ class ConversationFragment :
preUploadResults = result.preUploadResults 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 //endregion
@@ -3164,19 +3187,24 @@ class ConversationFragment :
private inner class AttachmentKeyboardFragmentListener : FragmentResultListener { private inner class AttachmentKeyboardFragmentListener : FragmentResultListener {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override fun onFragmentResult(requestKey: String, result: Bundle) { override fun onFragmentResult(requestKey: String, result: Bundle) {
val recipient = viewModel.recipientSnapshot ?: return
val button: AttachmentKeyboardButton? = result.getSerializable(AttachmentKeyboardFragment.BUTTON_RESULT) as? AttachmentKeyboardButton val button: AttachmentKeyboardButton? = result.getSerializable(AttachmentKeyboardFragment.BUTTON_RESULT) as? AttachmentKeyboardButton
val media: Media? = result.getParcelable(AttachmentKeyboardFragment.MEDIA_RESULT) val media: Media? = result.getParcelable(AttachmentKeyboardFragment.MEDIA_RESULT)
if (button != null) { if (button != null) {
when (button) { when (button) {
AttachmentKeyboardButton.GALLERY -> AttachmentManager.selectGallery(this@ConversationFragment, 1, viewModel.recipientSnapshot!!, composeText.textTrimmed, sendButton.selectedSendType, inputPanel.quote.isPresent) AttachmentKeyboardButton.GALLERY -> conversationActivityResultContracts.launchGallery(recipient.id, composeText.textTrimmed, inputPanel.quote.isPresent)
AttachmentKeyboardButton.FILE -> AttachmentManager.selectDocument(this@ConversationFragment, 1) AttachmentKeyboardButton.CONTACT -> conversationActivityResultContracts.launchSelectContact()
AttachmentKeyboardButton.CONTACT -> AttachmentManager.selectContactInfo(this@ConversationFragment, 1) AttachmentKeyboardButton.LOCATION -> conversationActivityResultContracts.launchSelectLocation(recipient.chatColors)
AttachmentKeyboardButton.LOCATION -> AttachmentManager.selectLocation(this@ConversationFragment, 1, viewModel.recipientSnapshot!!.chatColors.asSingleColor()) AttachmentKeyboardButton.PAYMENT -> AttachmentManager.selectPayment(this@ConversationFragment, recipient)
AttachmentKeyboardButton.PAYMENT -> AttachmentManager.selectPayment(this@ConversationFragment, viewModel.recipientSnapshot!!) AttachmentKeyboardButton.FILE -> {
if (!conversationActivityResultContracts.launchSelectFile()) {
toast(R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG)
}
}
} }
} else if (media != null) { } else if (media != null) {
conversationActivityResultContracts.launchMediaEditor(listOf(media), viewModel.recipientSnapshot!!.id, composeText.textTrimmed) conversationActivityResultContracts.launchMediaEditor(listOf(media), recipient.id, composeText.textTrimmed)
} }
container.hideInput() container.hideInput()

View File

@@ -64,7 +64,7 @@ public final class PlacePickerActivity extends AppCompatActivity {
private static final int ANIMATION_DURATION = 250; private static final int ANIMATION_DURATION = 250;
private static final OvershootInterpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator(); 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(); private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();