From 0ca8696e882abae40928a3d037785e2935638bb8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 11 Oct 2019 16:41:04 +0200 Subject: [PATCH] Attachments/Share: cleaning code and add contact picking --- vector/src/main/AndroidManifest.xml | 1 + .../riotx/core/utils/PermissionsTools.kt | 3 +- .../attachments/AttachmentTypeSelectorView.kt | 33 +++++++++++-------- .../features/attachments/AttachmentsHelper.kt | 24 +++++++++++++- .../features/attachments/AttachmentsMapper.kt | 22 ++++++++----- .../attachments/AttachmentsPickerCallback.kt | 25 +++++++++----- .../features/attachments/ContactAttachment.kt | 32 ++++++++++++++++++ .../attachments/PickerManagerFactory.kt | 17 ++++++++++ .../home/room/detail/RoomDetailFragment.kt | 15 ++++++--- .../features/share/IncomingShareActivity.kt | 13 +++----- .../layout/view_attachment_type_selector.xml | 12 +++---- vector/src/main/res/values/strings_riotX.xml | 9 +++++ 12 files changed, 153 insertions(+), 53 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1123335ceb..c56fc02eda 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="im.vector.riotx"> + (R.id.attachmentStickersButton).configure(Type.STICKER) audioButton = layout.findViewById(R.id.attachmentAudioButton).configure(Type.AUDIO) contactButton = layout.findViewById(R.id.attachmentContactButton).configure(Type.CONTACT) - contentView = layout + contentView = root width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT animationStyle = 0 @@ -197,7 +204,7 @@ class AttachmentTypeSelectorView(context: Context, } private fun ImageButton.configure(type: Type): ImageButton { - this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type)) + this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal)) this.setOnClickListener(TypeClickListener(type)) return this } @@ -211,19 +218,17 @@ class AttachmentTypeSelectorView(context: Context, } - enum class Type { - - CAMERA, - GALLERY, - FILE, - STICKER, - AUDIO, - CONTACT; - - fun requirePermission(): Boolean { - return this != CAMERA && this != STICKER - } + /** + * The all possible types to pick with their required permissions. + */ + enum class Type(val permissionsBit: Int) { + CAMERA(PERMISSIONS_EMPTY), + GALLERY(PERMISSIONS_FOR_WRITING_FILES), + FILE(PERMISSIONS_FOR_WRITING_FILES), + STICKER(PERMISSIONS_EMPTY), + AUDIO(PERMISSIONS_FOR_WRITING_FILES), + CONTACT(PERMISSIONS_FOR_PICKING_CONTACT) } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index 46f25ca3e2..89a397c441 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -24,6 +24,7 @@ import com.kbeanie.multipicker.core.PickerManager import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.core.platform.Restorable +import timber.log.Timber private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY" private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY" @@ -45,11 +46,17 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi } interface Callback { - fun onAttachmentsReady(attachments: List) + fun onContactAttachmentReady(contactAttachment: ContactAttachment) { + Timber.v("On contact attachment ready: $contactAttachment") + } + + fun onContentAttachmentsReady(attachments: List) fun onAttachmentsProcessFailed() } + // Capture path allows to handle camera image picking. It must be restored if the activity gets killed. private var capturePath: String? = null + // The pending type is set if we have to handle permission request. It must be restored if the activity gets killed. var pendingType: AttachmentTypeSelectorView.Type? = null private val imagePicker by lazy { @@ -72,6 +79,10 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi pickerManagerFactory.createAudioPicker() } + private val contactPicker by lazy { + pickerManagerFactory.createContactPicker() + } + // Restorable override fun onSaveInstanceState(outState: Bundle) { @@ -121,6 +132,13 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi capturePath = cameraImagePicker.pickImage() } + /** + * Starts the process for handling contact picking + */ + fun selectContact() { + contactPicker.pickContact() + } + /** * This methods aims to handle on activity result data. * @@ -148,6 +166,8 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else if (type.startsWith("video")) { videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + } else if (type.startsWith("audio")) { + videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) { filePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else { @@ -161,6 +181,8 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi PICK_IMAGE_DEVICE -> imagePicker PICK_IMAGE_CAMERA -> cameraImagePicker PICK_FILE -> filePicker + PICK_CONTACT -> contactPicker + PICK_AUDIO -> audioPicker else -> null } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index ca46936af9..5536e2094b 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -16,12 +16,18 @@ package im.vector.riotx.features.attachments -import com.kbeanie.multipicker.api.entity.ChosenAudio -import com.kbeanie.multipicker.api.entity.ChosenFile -import com.kbeanie.multipicker.api.entity.ChosenImage -import com.kbeanie.multipicker.api.entity.ChosenVideo +import com.kbeanie.multipicker.api.entity.* import im.vector.matrix.android.api.session.content.ContentAttachmentData +fun ChosenContact.toContactAttachment(): ContactAttachment { + return ContactAttachment( + displayName = displayName, + photoUri = photoUri, + emails = emails.toList(), + phones = phones.toList() + ) +} + fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { return ContentAttachmentData( path = originalPath, @@ -61,8 +67,8 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { type = mapType(), name = displayName, size = size, - height = height?.toLong(), - width = width?.toLong(), + height = height.toLong(), + width = width.toLong(), date = createdAt?.time ?: System.currentTimeMillis() ) } @@ -74,8 +80,8 @@ fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { type = ContentAttachmentData.Type.VIDEO, size = size, date = createdAt?.time ?: System.currentTimeMillis(), - height = height?.toLong(), - width = width?.toLong(), + height = height.toLong(), + width = width.toLong(), duration = duration, name = displayName ) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt index 7df0b6f15b..e1fb2eec65 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt @@ -21,15 +21,22 @@ import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback import com.kbeanie.multipicker.api.callbacks.FilePickerCallback import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback -import com.kbeanie.multipicker.api.entity.ChosenAudio -import com.kbeanie.multipicker.api.entity.ChosenFile -import com.kbeanie.multipicker.api.entity.ChosenImage -import com.kbeanie.multipicker.api.entity.ChosenVideo +import com.kbeanie.multipicker.api.entity.* +import timber.log.Timber /** * This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback] */ -class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) : ImagePickerCallback, FilePickerCallback, VideoPickerCallback, AudioPickerCallback { +class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) : ImagePickerCallback, FilePickerCallback, VideoPickerCallback, AudioPickerCallback, ContactPickerCallback { + + override fun onContactChosen(contact: ChosenContact?) { + if (contact == null) { + callback.onAttachmentsProcessFailed() + } else { + val contactAttachment = contact.toContactAttachment() + callback.onContactAttachmentReady(contactAttachment) + } + } override fun onAudiosChosen(audios: MutableList?) { if (audios.isNullOrEmpty()) { @@ -38,7 +45,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = audios.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } @@ -49,7 +56,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = files.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } @@ -60,7 +67,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = images.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } @@ -71,7 +78,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = videos.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt new file mode 100644 index 0000000000..dbbed4d5fc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt @@ -0,0 +1,32 @@ +package im.vector.riotx.features.attachments + +/** + * Data class holding values of a picked contact + * Can be send as a text message waiting for the protocol to handle contact. + */ +data class ContactAttachment( + val displayName: String, + val photoUri: String?, + val phones: List = emptyList(), + val emails: List = emptyList() +) { + + fun toHumanReadable(): String { + val stringBuilder = StringBuilder(displayName) + phones.concatIn(stringBuilder) + emails.concatIn(stringBuilder) + return stringBuilder.toString() + } + + private fun List.concatIn(stringBuilder: StringBuilder) { + if (isNotEmpty()) { + stringBuilder.append("\n") + for (i in 0 until size - 1) { + val value = get(i) + stringBuilder.append(value).append("\n") + } + stringBuilder.append(last()) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt index 23344b81ba..8bfe44308d 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt @@ -25,6 +25,9 @@ import com.kbeanie.multipicker.api.FilePicker import com.kbeanie.multipicker.api.ImagePicker import com.kbeanie.multipicker.api.VideoPicker +/** + * Factory for creating different pickers. It allows to use with fragment or activity builders. + */ interface PickerManagerFactory { fun createImagePicker(): ImagePicker @@ -37,6 +40,8 @@ interface PickerManagerFactory { fun createAudioPicker(): AudioPicker + fun createContactPicker(): ContactPicker + } class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory { @@ -76,6 +81,12 @@ class ActivityPickerManagerFactory(private val activity: Activity, callback: Att it.setAudioPickerCallback(attachmentsPickerCallback) } } + + override fun createContactPicker(): ContactPicker { + return ContactPicker(activity).also { + it.setContactPickerCallback(attachmentsPickerCallback) + } + } } class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory { @@ -116,5 +127,11 @@ class FragmentPickerManagerFactory(private val fragment: Fragment, callback: Att } } + override fun createContactPicker(): ContactPicker { + return ContactPicker(fragment).also { + it.setContactPickerCallback(attachmentsPickerCallback) + } + } + } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index bdc6408e88..7b8a80939c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -80,6 +80,7 @@ import im.vector.riotx.core.utils.Debouncer import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper +import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter @@ -1120,7 +1121,7 @@ class RoomDetailFragment : // AttachmentTypeSelectorView.Callback override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { - if (!type.requirePermission() || checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) { + if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) { launchAttachmentProcess(type) } else { attachmentsHelper.pendingType = type @@ -1133,19 +1134,23 @@ class RoomDetailFragment : AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() - AttachmentTypeSelectorView.Type.CONTACT -> vectorBaseActivity.notImplemented("Picking contacts") + AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact() AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") } } // AttachmentsHelper.Callback - override fun onAttachmentsReady(attachments: List) { - Timber.v("onAttachmentsReady") + override fun onContentAttachmentsReady(attachments: List) { roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments)) } override fun onAttachmentsProcessFailed() { - Timber.v("onAttachmentsProcessFailed") + Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show() + } + + override fun onContactAttachmentReady(contactAttachment: ContactAttachment) { + val formattedContact = contactAttachment.toHumanReadable() + roomDetailViewModel.process(RoomDetailActions.SendMessage(formattedContact, false)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 77fece859d..e5fbebf824 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -2,7 +2,6 @@ package im.vector.riotx.features.share import android.content.ClipDescription import android.content.Intent -import android.net.Uri import android.os.Bundle import android.widget.Toast import im.vector.matrix.android.api.session.content.ContentAttachmentData @@ -12,12 +11,10 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.attachments.AttachmentsHelper -import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.LoadingFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.login.LoginActivity -import im.vector.riotx.features.login.LoginConfig import kotlinx.android.synthetic.main.activity_incoming_share.* import javax.inject.Inject @@ -25,7 +22,6 @@ import javax.inject.Inject class IncomingShareActivity : VectorBaseActivity(), AttachmentsHelper.Callback { - @Inject lateinit var sessionHolder: ActiveSessionHolder private lateinit var roomListFragment: RoomListFragment private lateinit var attachmentsHelper: AttachmentsHelper @@ -40,6 +36,8 @@ class IncomingShareActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // If we are not logged in, stop the sharing process and open login screen. + // In the future, we might want to relaunch the sharing process after login. if (!sessionHolder.hasActiveSession()) { startLoginActivity() return @@ -63,7 +61,7 @@ class IncomingShareActivity : } } - override fun onAttachmentsReady(attachments: List) { + override fun onContentAttachmentsReady(attachments: List) { val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments)) roomListFragment = RoomListFragment.newInstance(roomListParams) replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer) @@ -74,7 +72,7 @@ class IncomingShareActivity : } private fun cantManageShare() { - Toast.makeText(this, "Couldn't handle share data", Toast.LENGTH_LONG).show() + Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show() finish() } @@ -93,9 +91,6 @@ class IncomingShareActivity : return false } - /** - * Start the login screen with identity server and home server pre-filled - */ private fun startLoginActivity() { val intent = LoginActivity.newIntent(this, null) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml index 772407ba3f..2af86d6c0d 100644 --- a/vector/src/main/res/layout/view_attachment_type_selector.xml +++ b/vector/src/main/res/layout/view_attachment_type_selector.xml @@ -34,7 +34,7 @@ + android:text="@string/attachment_type_camera" /> @@ -54,7 +54,7 @@ + android:text="@string/attachment_type_gallery" /> @@ -74,7 +74,7 @@ + android:text="@string/attachment_type_file" /> @@ -103,7 +103,7 @@ + android:text="@string/attachment_type_audio" /> @@ -123,7 +123,7 @@ + android:text="@string/attachment_type_contact" /> @@ -143,7 +143,7 @@ + android:text="@string/attachment_type_sticker" /> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 370b17be22..6b861a1ecc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -37,4 +37,13 @@ "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." + "An error occurred while retrieving the attachment." + "File" + "Contact" + "Camera" + "Audio" + "Gallery" + "Sticker" + Couldn\'t handle share data +