Attachments/Share: cleaning code and add contact picking

This commit is contained in:
ganfra 2019-10-11 16:41:04 +02:00
parent ee5ebb4b83
commit 0ca8696e88
12 changed files with 153 additions and 53 deletions

View file

@ -4,6 +4,7 @@
package="im.vector.riotx">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:name=".VectorApplication"

View file

@ -54,8 +54,9 @@ const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA
const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE
const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS
private const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
// Request code to ask permission to the system (arbitrary values)
const val PERMISSION_REQUEST_CODE = 567

View file

@ -41,10 +41,17 @@ import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import im.vector.riotx.R
import im.vector.riotx.core.extensions.getMeasurements
import im.vector.riotx.core.utils.PERMISSIONS_EMPTY
import im.vector.riotx.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import kotlin.math.max
private const val ANIMATION_DURATION = 250
/**
* This class is the view presenting choices for picking attachments.
* It will return result through [Callback].
*/
class AttachmentTypeSelectorView(context: Context,
inflater: LayoutInflater,
var callback: Callback?)
@ -74,7 +81,7 @@ class AttachmentTypeSelectorView(context: Context,
stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(Type.STICKER)
audioButton = layout.findViewById<ImageButton>(R.id.attachmentAudioButton).configure(Type.AUDIO)
contactButton = layout.findViewById<ImageButton>(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)
}

View file

@ -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<ContentAttachmentData>)
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
Timber.v("On contact attachment ready: $contactAttachment")
}
fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>)
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
}
}

View file

@ -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
)

View file

@ -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<ChosenAudio>?) {
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)
}
}

View file

@ -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<String> = emptyList(),
val emails: List<String> = emptyList()
) {
fun toHumanReadable(): String {
val stringBuilder = StringBuilder(displayName)
phones.concatIn(stringBuilder)
emails.concatIn(stringBuilder)
return stringBuilder.toString()
}
private fun List<String>.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())
}
}
}

View file

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

View file

@ -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<ContentAttachmentData>) {
Timber.v("onAttachmentsReady")
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
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))
}
}

View file

@ -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<ContentAttachmentData>) {
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
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)

View file

@ -34,7 +34,7 @@
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="Camera" />
android:text="@string/attachment_type_camera" />
</LinearLayout>
@ -54,7 +54,7 @@
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="Gallery" />
android:text="@string/attachment_type_gallery" />
</LinearLayout>
@ -74,7 +74,7 @@
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="File" />
android:text="@string/attachment_type_file" />
</LinearLayout>
@ -103,7 +103,7 @@
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="Audio" />
android:text="@string/attachment_type_audio" />
</LinearLayout>
@ -123,7 +123,7 @@
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="Contact" />
android:text="@string/attachment_type_contact" />
</LinearLayout>
@ -143,7 +143,7 @@
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="Stickers" />
android:text="@string/attachment_type_sticker" />
</LinearLayout>
</LinearLayout>

View file

@ -37,4 +37,13 @@
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
<string name="error_attachment">"An error occurred while retrieving the attachment."</string>
<string name="attachment_type_file">"File"</string>
<string name="attachment_type_contact">"Contact"</string>
<string name="attachment_type_camera">"Camera"</string>
<string name="attachment_type_audio">"Audio"</string>
<string name="attachment_type_gallery">"Gallery"</string>
<string name="attachment_type_sticker">"Sticker"</string>
<string name="error_handling_incoming_share">Couldn\'t handle share data</string>
</resources>