mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Properly plumb attachment keyboard in CFv2.
This commit is contained in:
committed by
Greyson Parrelli
parent
36fc9aa82a
commit
cfaef77b21
@@ -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?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user