From 3073470c386d6b323e58beabd1f0b4fe949e6ca9 Mon Sep 17 00:00:00 2001
From: ganfra <francoisg@matrix.org>
Date: Tue, 8 Oct 2019 19:59:09 +0200
Subject: [PATCH] Attachments: start working on new UI (using system file
 picker) [WIP]

---
 .../riotx/features/attachments/Attachment.kt  |  50 +++++
 .../attachments/AttachmentTypeSelectorView.kt | 200 ++++++++++++++++++
 .../features/attachments/AttachmentsHelper.kt | 152 +++++++++++++
 .../home/room/detail/RoomDetailActions.kt     |   4 +-
 .../home/room/detail/RoomDetailFragment.kt    | 145 +++++--------
 .../home/room/detail/RoomDetailViewModel.kt   |  10 +-
 .../ic_attachment_camera_white_24dp.xml       |   4 +
 .../ic_attachment_file_white_24dp.xml         |   4 +
 .../ic_attachment_gallery_white_24dp.xml      |   4 +
 .../res/layout/attachment_type_selector.xml   | 114 ++++++++++
 .../src/main/res/xml/riotx_provider_paths.xml |   5 +
 11 files changed, 590 insertions(+), 102 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/Attachment.kt
 create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
 create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
 create mode 100644 vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml
 create mode 100644 vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml
 create mode 100644 vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml
 create mode 100644 vector/src/main/res/layout/attachment_type_selector.xml

diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/Attachment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/Attachment.kt
new file mode 100644
index 0000000000..37640ad179
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/attachments/Attachment.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.attachments
+
+import im.vector.riotx.core.resources.MIME_TYPE_ALL_CONTENT
+
+data class Attachment(val path: String,
+                      val mimeType: String,
+                      val name: String? = "",
+                      val width: Long? = 0,
+                      val height: Long? = 0,
+                      val size: Long = 0,
+                      val duration: Long? = 0,
+                      val date: Long = 0) {
+
+    val type: Int
+        get() {
+            if (mimeType == null) {
+                return TYPE_FILE
+            }
+            return when {
+                mimeType.startsWith("image/") -> TYPE_IMAGE
+                mimeType.startsWith("video/") -> TYPE_VIDEO
+                mimeType.startsWith("audio/")
+                                              -> TYPE_AUDIO
+                else                          -> TYPE_FILE
+            }
+        }
+
+    companion object {
+        val TYPE_FILE = 0
+        val TYPE_IMAGE = 1
+        val TYPE_AUDIO = 2
+        val TYPE_VIDEO = 3
+    }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
new file mode 100644
index 0000000000..9acdd6ae1f
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.attachments
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.annotation.TargetApi
+import android.content.Context
+import android.graphics.drawable.BitmapDrawable
+import android.os.Build
+import android.util.Pair
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewAnimationUtils
+import android.view.animation.Animation
+import android.view.animation.AnimationSet
+import android.view.animation.OvershootInterpolator
+import android.view.animation.ScaleAnimation
+import android.view.animation.TranslateAnimation
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.PopupWindow
+import androidx.core.view.doOnNextLayout
+import com.amulyakhare.textdrawable.TextDrawable
+import com.amulyakhare.textdrawable.util.ColorGenerator
+import im.vector.riotx.R
+import kotlin.math.max
+
+class AttachmentTypeSelectorView(context: Context, var callback: Callback?)
+    : PopupWindow(context) {
+
+    interface Callback {
+        fun onTypeSelected(type: Int)
+    }
+
+    private val iconColorGenerator = ColorGenerator.MATERIAL
+
+    private var galleryButton: ImageButton
+    private var cameraButton: ImageButton
+    private var fileButton: ImageButton
+    private var stickersButton: ImageButton
+
+    private var anchor: View? = null
+
+    init {
+        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+        val layout = inflater.inflate(R.layout.attachment_type_selector, null, true)
+        galleryButton = layout.findViewById<ImageButton>(R.id.attachmentGalleryButton).configure(TYPE_GALLERY)
+        cameraButton = layout.findViewById<ImageButton>(R.id.attachmentCameraButton).configure(TYPE_CAMERA)
+        fileButton = layout.findViewById<ImageButton>(R.id.attachmentFileButton).configure(TYPE_FILE)
+        stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(TYPE_STICKER)
+        contentView = layout
+        width = LinearLayout.LayoutParams.MATCH_PARENT
+        height = LinearLayout.LayoutParams.WRAP_CONTENT
+        setBackgroundDrawable(BitmapDrawable())
+        animationStyle = 0
+        inputMethodMode = INPUT_METHOD_NOT_NEEDED
+        isFocusable = true
+        isTouchable = true
+    }
+
+    fun show(anchor: View) {
+        showAtLocation(anchor, Gravity.BOTTOM, 0, 0)
+        contentView.doOnNextLayout {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                animateWindowInCircular(anchor, contentView)
+            } else {
+                animateWindowInTranslate(contentView)
+            }
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            animateButtonIn(galleryButton, ANIMATION_DURATION / 2)
+            animateButtonIn(cameraButton, ANIMATION_DURATION / 2)
+            animateButtonIn(fileButton, ANIMATION_DURATION / 4)
+            animateButtonIn(stickersButton, 0)
+        }
+    }
+
+    override fun dismiss() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            animateWindowOutCircular(anchor, contentView)
+        } else {
+            animateWindowOutTranslate(contentView)
+        }
+    }
+
+    private fun animateButtonIn(button: View, delay: Int) {
+        val animation = AnimationSet(true)
+        val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
+        animation.addAnimation(scale)
+        animation.interpolator = OvershootInterpolator(1f)
+        animation.duration = ANIMATION_DURATION.toLong()
+        animation.startOffset = delay.toLong()
+        button.startAnimation(animation)
+    }
+
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun animateWindowInCircular(anchor: View?, contentView: View) {
+        val coordinates = getClickCoordinates(anchor, contentView)
+        val animator = ViewAnimationUtils.createCircularReveal(contentView,
+                                                               coordinates.first,
+                                                               coordinates.second,
+                                                               0f,
+                                                               max(contentView.width, contentView.height).toFloat())
+        animator.duration = ANIMATION_DURATION.toLong()
+        animator.start()
+    }
+
+    private fun animateWindowInTranslate(contentView: View) {
+        val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
+        animation.duration = ANIMATION_DURATION.toLong()
+        getContentView().startAnimation(animation)
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun animateWindowOutCircular(anchor: View?, contentView: View) {
+        val coordinates = getClickCoordinates(anchor, contentView)
+        val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
+                                                               coordinates.first,
+                                                               coordinates.second,
+                                                               max(getContentView().width, getContentView().height).toFloat(),
+                                                               0f)
+
+        animator.duration = ANIMATION_DURATION.toLong()
+        animator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                super@AttachmentTypeSelectorView.dismiss()
+            }
+        })
+        animator.start()
+    }
+
+    private fun animateWindowOutTranslate(contentView: View) {
+        val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat())
+        animation.duration = ANIMATION_DURATION.toLong()
+        animation.setAnimationListener(object : Animation.AnimationListener {
+            override fun onAnimationStart(animation: Animation) {}
+
+            override fun onAnimationEnd(animation: Animation) {
+                super@AttachmentTypeSelectorView.dismiss()
+            }
+
+            override fun onAnimationRepeat(animation: Animation) {}
+        })
+
+        getContentView().startAnimation(animation)
+    }
+
+    private fun getClickCoordinates(anchor: View?, contentView: View): Pair<Int, Int> {
+        val anchorCoordinates = IntArray(2)
+        anchor?.getLocationOnScreen(anchorCoordinates)
+        val contentCoordinates = IntArray(2)
+        contentView.getLocationOnScreen(contentCoordinates)
+        val x = anchorCoordinates[0] - contentCoordinates[0]
+        val y = anchorCoordinates[1] - contentCoordinates[1]
+        return Pair(x, y)
+    }
+
+    private fun ImageButton.configure(type: Int): ImageButton {
+        this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type))
+        this.setOnClickListener(TypeClickListener(type))
+        return this
+    }
+
+    private inner class TypeClickListener(private val type: Int) : View.OnClickListener {
+
+        override fun onClick(v: View) {
+            dismiss()
+            callback?.onTypeSelected(type)
+        }
+
+    }
+
+    companion object {
+
+        const val TYPE_CAMERA = 0
+        const val TYPE_GALLERY = 1
+        const val TYPE_FILE = 2
+        const val TYPE_STICKER = 3
+
+        private const val ANIMATION_DURATION = 250
+    }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..042ec7da79
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotx.features.attachments
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Environment
+import android.provider.MediaStore
+import android.provider.OpenableColumns
+import androidx.core.content.FileProvider
+import androidx.fragment.app.Fragment
+import im.vector.riotx.BuildConfig
+import im.vector.riotx.core.resources.MIME_TYPE_ALL_CONTENT
+import timber.log.Timber
+import java.io.File
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.*
+
+
+class AttachmentsHelper(private val context: Context) {
+
+    private var capturePath: String? = null
+
+    fun selectFile(fragment: Fragment, requestCode: Int) {
+        selectMediaType(fragment, "*/*", null, requestCode)
+    }
+
+    fun selectGallery(fragment: Fragment, requestCode: Int) {
+        selectMediaType(fragment, "image/*", arrayOf("image/*", "video/*"), requestCode)
+    }
+
+    fun openCamera(fragment: Fragment, requestCode: Int) {
+        dispatchTakePictureIntent(fragment, requestCode)
+    }
+
+
+    fun handleOpenCameraResult(): List<Attachment> {
+        val attachment = getAttachmentFromContentResolver(Uri.parse(capturePath))
+        return if (attachment == null) {
+            emptyList()
+        } else {
+            listOf(attachment)
+        }
+    }
+
+    fun handleSelectResult(data: Intent?): List<Attachment> {
+        val clipData = data?.clipData
+        if (clipData != null) {
+            return (0 until clipData.itemCount).map {
+                clipData.getItemAt(it)
+            }.mapNotNull {
+                getAttachmentFromContentResolver(it.uri)
+            }
+        } else {
+            val uri = data?.data ?: return emptyList()
+            val attachment = getAttachmentFromContentResolver(uri)
+            return if (attachment == null) {
+                emptyList()
+            } else {
+                listOf(attachment)
+            }
+        }
+    }
+
+    private fun selectMediaType(fragment: Fragment, type: String, extraMimeType: Array<String>?, requestCode: Int) {
+        val intent = Intent()
+        intent.type = type
+        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+        if (extraMimeType != null) {
+            intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeType)
+        }
+        intent.action = Intent.ACTION_OPEN_DOCUMENT
+        try {
+            fragment.startActivityForResult(intent, requestCode)
+            return
+        } catch (exception: ActivityNotFoundException) {
+            Timber.e(exception)
+        }
+        intent.action = Intent.ACTION_GET_CONTENT
+        try {
+            fragment.startActivityForResult(intent, requestCode)
+        } catch (exception: ActivityNotFoundException) {
+            Timber.e(exception)
+        }
+    }
+
+    private fun getAttachmentFromContentResolver(uri: Uri): Attachment? {
+        return context.contentResolver.query(uri, null, null, null, null)?.use {
+            if (it.moveToFirst()) {
+                val fileName = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
+                val fileSize = it.getLong(it.getColumnIndex(OpenableColumns.SIZE))
+                val mimeType = context.contentResolver.getType(uri) ?: MIME_TYPE_ALL_CONTENT
+                Attachment(uri.toString(), mimeType, fileName, fileSize)
+            } else {
+                null
+            }
+        }
+    }
+
+
+    @Throws(IOException::class)
+    private fun createImageFile(context: Context): File {
+        val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
+        val imageFileName = "JPEG_" + timeStamp + "_"
+        val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
+        val image = File.createTempFile(
+                imageFileName, /* prefix */
+                ".jpg", /* suffix */
+                storageDir      /* directory */
+        )
+        // Save a file: path for use with ACTION_VIEW intents
+        capturePath = image.absolutePath
+        return image
+    }
+
+    private fun dispatchTakePictureIntent(fragment: Fragment, requestCode: Int) {
+        val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+        // Ensure that there's a camera activity to handle the intent
+        if (takePictureIntent.resolveActivity(fragment.requireActivity().packageManager) != null) {
+            // Create the File where the photo should go
+            var photoFile: File? = null
+            try {
+                photoFile = createImageFile(fragment.requireContext())
+            } catch (ex: IOException) {
+                Timber.e(ex, "Couldn't create image file")
+            }
+            // Continue only if the File was successfully created
+            if (photoFile != null) {
+                val photoURI = FileProvider.getUriForFile(fragment.requireContext(), BuildConfig.APPLICATION_ID + ".fileProvider", photoFile)
+                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
+                fragment.startActivityForResult(takePictureIntent, requestCode)
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
index 886c3cdfaf..da11e0fbc3 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
@@ -16,17 +16,17 @@
 
 package im.vector.riotx.features.home.room.detail
 
-import com.jaiselrahman.filepicker.model.MediaFile
 import im.vector.matrix.android.api.session.events.model.Event
 import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
 import im.vector.matrix.android.api.session.room.timeline.Timeline
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.riotx.features.attachments.Attachment
 
 sealed class RoomDetailActions {
 
     data class SaveDraft(val draft: String) : RoomDetailActions()
     data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
-    data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
+    data class SendMedia(val attachments: List<Attachment>) : RoomDetailActions()
     data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
     data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
     data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
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 ea5dc83997..f543868656 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
@@ -50,7 +50,6 @@ import com.github.piasy.biv.BigImageViewer
 import com.github.piasy.biv.loader.ImageLoader
 import com.google.android.material.snackbar.Snackbar
 import com.jaiselrahman.filepicker.activity.FilePickerActivity
-import com.jaiselrahman.filepicker.config.Configurations
 import com.jaiselrahman.filepicker.model.MediaFile
 import com.otaliastudios.autocomplete.Autocomplete
 import com.otaliastudios.autocomplete.AutocompleteCallback
@@ -67,7 +66,6 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
 import im.vector.matrix.android.api.session.user.model.User
 import im.vector.riotx.R
 import im.vector.riotx.core.di.ScreenComponent
-import im.vector.riotx.core.dialogs.DialogListItem
 import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
 import im.vector.riotx.core.error.ErrorFormatter
 import im.vector.riotx.core.extensions.hideKeyboard
@@ -81,6 +79,8 @@ import im.vector.riotx.core.ui.views.NotificationAreaView
 import im.vector.riotx.core.utils.*
 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.autocomplete.command.AutocompleteCommandPresenter
 import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
 import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@@ -125,17 +125,18 @@ data class RoomDetailArgs(
 ) : Parcelable
 
 
-private const val CAMERA_VALUE_TITLE = "attachment"
-private const val REQUEST_FILES_REQUEST_CODE = 0
-private const val TAKE_IMAGE_REQUEST_CODE = 1
-private const val REACTION_SELECT_REQUEST_CODE = 2
+private const val REQUEST_CODE_SELECT_FILE = 1
+private const val REQUEST_CODE_SELECT_GALLERY = 2
+private const val REQUEST_CODE_OPEN_CAMERA = 3
+private const val REACTION_SELECT_REQUEST_CODE = 4
 
 class RoomDetailFragment :
         VectorBaseFragment(),
         TimelineEventController.Callback,
         AutocompleteUserPresenter.Callback,
         VectorInviteView.Callback,
-        JumpToReadMarkerView.Callback {
+        JumpToReadMarkerView.Callback,
+        AttachmentTypeSelectorView.Callback {
 
     companion object {
 
@@ -197,9 +198,11 @@ class RoomDetailFragment :
 
     private lateinit var actionViewModel: ActionsHandler
     private lateinit var layoutManager: LinearLayoutManager
+    private lateinit var attachmentsHelper: AttachmentsHelper
 
     @BindView(R.id.composerLayout)
     lateinit var composerLayout: TextComposerView
+    private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
 
     private var lockSendButton = false
 
@@ -210,6 +213,7 @@ class RoomDetailFragment :
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         super.onActivityCreated(savedInstanceState)
         actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
+        attachmentsHelper = AttachmentsHelper((requireActivity()))
         setupToolbar(roomToolbar)
         setupRecyclerView()
         setupComposer()
@@ -298,9 +302,9 @@ class RoomDetailFragment :
         AlertDialog.Builder(requireActivity())
                 .setTitle(R.string.dialog_title_error)
                 .setMessage(getString(R.string.error_file_too_big,
-                        error.filename,
-                        TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
-                        TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
+                                      error.filename,
+                                      TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
+                                      TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
                 ))
                 .setPositiveButton(R.string.ok, null)
                 .show()
@@ -362,7 +366,7 @@ class RoomDetailFragment :
 
     private fun renderSpecialMode(event: TimelineEvent,
                                   @DrawableRes iconRes: Int,
-                                 descriptionRes: Int,
+                                  descriptionRes: Int,
                                   defaultContent: String) {
         commandAutocompletePolicy.enabled = false
         //switch to expanded bar
@@ -426,23 +430,31 @@ class RoomDetailFragment :
     }
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        super.onActivityResult(requestCode, resultCode, data)
-        if (resultCode == RESULT_OK && data != null) {
-            when (requestCode) {
-                REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
-                REACTION_SELECT_REQUEST_CODE                        -> {
-                    val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
-                                  ?: return
-                    val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
-                                   ?: return
-                    //TODO check if already reacted with that?
-                    roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
+        if (resultCode == RESULT_OK) {
+            if (requestCode == REQUEST_CODE_OPEN_CAMERA) {
+                val attachments = attachmentsHelper.handleOpenCameraResult()
+                roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
+            } else if (data != null) {
+                when (requestCode) {
+                    REQUEST_CODE_SELECT_FILE,
+                    REQUEST_CODE_SELECT_GALLERY  -> {
+                        val attachments = attachmentsHelper.handleSelectResult(data)
+                        roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
+                    }
+                    REACTION_SELECT_REQUEST_CODE -> {
+                        val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
+                                      ?: return
+                        val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
+                                       ?: return
+                        //TODO check if already reacted with that?
+                        roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
+                    }
                 }
             }
         }
     }
 
-// PRIVATE METHODS *****************************************************************************
+    // PRIVATE METHODS *****************************************************************************
 
 
     private fun setupRecyclerView() {
@@ -610,43 +622,10 @@ class RoomDetailFragment :
 
     private fun setupAttachmentButton() {
         composerLayout.attachmentButton.setOnClickListener {
-            val intent = Intent(requireContext(), FilePickerActivity::class.java)
-            intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
-                    .setCheckPermission(true)
-                    .setShowFiles(true)
-                    .setShowAudios(true)
-                    .setSkipZeroSizeFiles(true)
-                    .build())
-            startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
-            /*
-            val items = ArrayList<DialogListItem>()
-            // Send file
-            items.add(DialogListItem.SendFile)
-            // Send voice
-
-            if (vectorPreferences.isSendVoiceFeatureEnabled()) {
-                items.add(DialogListItem.SendVoice.INSTANCE)
+            if (!::attachmentTypeSelector.isInitialized) {
+                attachmentTypeSelector = AttachmentTypeSelectorView(requireContext(), this)
             }
-
-
-            // Send sticker
-            //items.add(DialogListItem.SendSticker)
-            // Camera
-
-            //if (vectorPreferences.useNativeCamera()) {
-            items.add(DialogListItem.TakePhoto)
-            items.add(DialogListItem.TakeVideo)
-            //} else {
-    //                items.add(DialogListItem.TakePhotoVideo.INSTANCE)
-            //          }
-            val adapter = DialogSendItemAdapter(requireContext(), items)
-            AlertDialog.Builder(requireContext())
-                    .setAdapter(adapter) { _, position ->
-                        onSendChoiceClicked(items[position])
-                    }
-                    .setNegativeButton(R.string.cancel, null)
-                    .show()
-                    */
+            attachmentTypeSelector.show(it)
         }
     }
 
@@ -654,38 +633,6 @@ class RoomDetailFragment :
         inviteView.callback = this
     }
 
-    private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
-        Timber.v("On send choice clicked: $dialogListItem")
-        when (dialogListItem) {
-            is DialogListItem.SendFile       -> {
-                // launchFileIntent
-            }
-            is DialogListItem.SendVoice      -> {
-                //launchAudioRecorderIntent()
-            }
-            is DialogListItem.SendSticker    -> {
-                //startStickerPickerActivity()
-            }
-            is DialogListItem.TakePhotoVideo ->
-                if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
-                    //    launchCamera()
-                }
-            is DialogListItem.TakePhoto      ->
-                if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
-                    openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
-                }
-            is DialogListItem.TakeVideo      ->
-                if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
-                    //  launchNativeVideoRecorder()
-                }
-        }
-    }
-
-    private fun handleMediaIntent(data: Intent) {
-        val files: ArrayList<MediaFile> = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES)
-        roomDetailViewModel.process(RoomDetailActions.SendMedia(files))
-    }
-
     private fun renderState(state: RoomDetailViewState) {
         readMarkerHelper.updateWith(state)
         renderRoomSummary(state)
@@ -973,7 +920,7 @@ class RoomDetailFragment :
     }
 
 
-    // AutocompleteUserPresenter.Callback
+// AutocompleteUserPresenter.Callback
 
     override fun onQueryUsers(query: CharSequence?) {
         textComposerViewModel.process(TextComposerActions.QueryUsers(query))
@@ -1139,7 +1086,7 @@ class RoomDetailFragment :
     }
 
 
-    // VectorInviteView.Callback
+// VectorInviteView.Callback
 
     override fun onAcceptInvite() {
         notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
@@ -1151,7 +1098,7 @@ class RoomDetailFragment :
         roomDetailViewModel.process(RoomDetailActions.RejectInvite)
     }
 
-    // JumpToReadMarkerView.Callback
+// JumpToReadMarkerView.Callback
 
     override fun onJumpToReadMarkerClicked(readMarkerId: String) {
         roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false))
@@ -1161,4 +1108,14 @@ class RoomDetailFragment :
         roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
     }
 
+// AttachmentTypeSelectorView.Callback *********************************************************
+
+    override fun onTypeSelected(type: Int) {
+        when (type) {
+            AttachmentTypeSelectorView.TYPE_CAMERA  -> attachmentsHelper.openCamera(this, REQUEST_CODE_OPEN_CAMERA)
+            AttachmentTypeSelectorView.TYPE_FILE    -> attachmentsHelper.selectFile(this, REQUEST_CODE_SELECT_FILE)
+            AttachmentTypeSelectorView.TYPE_GALLERY -> attachmentsHelper.selectGallery(this, REQUEST_CODE_SELECT_GALLERY)
+            AttachmentTypeSelectorView.TYPE_STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index 4b8e46b0d9..e86238bd8b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -46,7 +46,6 @@ import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneCo
 import im.vector.matrix.android.api.session.room.send.UserDraft
 import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
 import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
-import im.vector.matrix.android.api.util.Optional
 import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
 import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
 import im.vector.matrix.rx.rx
@@ -63,8 +62,6 @@ import im.vector.riotx.features.command.CommandParser
 import im.vector.riotx.features.command.ParsedCommand
 import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
 import im.vector.riotx.features.settings.VectorPreferences
-import io.reactivex.Observable
-import io.reactivex.functions.BiFunction
 import io.reactivex.rxkotlin.subscribeBy
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
@@ -469,7 +466,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     }
 
     private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
-        val attachments = action.mediaFiles.map {
+        val attachments = action.attachments.map {
             val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path))
 
             ContentAttachmentData(
@@ -481,7 +478,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                     name = nameWithExtension ?: it.name,
                     path = it.path,
                     mimeType = it.mimeType,
-                    type = ContentAttachmentData.Type.values()[it.mediaType]
+                    type = ContentAttachmentData.Type.values()[it.type]
             )
         }
 
@@ -495,7 +492,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         } else {
             when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
                 null -> room.sendMedias(attachments)
-                else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
+                else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
+                                                                             ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
             }
         }
     }
diff --git a/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml
new file mode 100644
index 0000000000..5c2920d252
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
+    <path android:fillColor="#FFFFFF" android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml
new file mode 100644
index 0000000000..4e6b9458f8
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
+    <path android:fillColor="#FFFFFF" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml
new file mode 100644
index 0000000000..d4e68f125b
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
+    <path android:fillColor="#FFFFFF" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
+</vector>
diff --git a/vector/src/main/res/layout/attachment_type_selector.xml b/vector/src/main/res/layout/attachment_type_selector.xml
new file mode 100644
index 0000000000..603cb9d72d
--- /dev/null
+++ b/vector/src/main/res/layout/attachment_type_selector.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@color/riotx_background_light">
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="16dp"
+        android:baselineAligned="false"
+        android:orientation="horizontal"
+        android:weightSum="4">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageButton
+                android:id="@+id/attachmentCameraButton"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:scaleType="center"
+                android:src="@drawable/ic_attachment_camera_white_24dp"
+                tools:background="@color/colorAccent" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:text="Camera" />
+
+        </LinearLayout>
+
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageButton
+                android:id="@+id/attachmentGalleryButton"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:scaleType="center"
+                android:src="@drawable/ic_attachment_gallery_white_24dp"
+                tools:background="@color/colorAccent" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:text="Gallery" />
+
+        </LinearLayout>
+
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageButton
+                android:id="@+id/attachmentFileButton"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:scaleType="center"
+                android:src="@drawable/ic_attachment_file_white_24dp"
+                tools:background="@color/colorAccent" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:text="File" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageButton
+                android:id="@+id/attachmentStickersButton"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:scaleType="center"
+                android:src="@drawable/ic_send_sticker"
+                tools:background="@color/colorAccent" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:text="Stickers" />
+
+        </LinearLayout>
+
+
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/vector/src/main/res/xml/riotx_provider_paths.xml b/vector/src/main/res/xml/riotx_provider_paths.xml
index 7d3fcb2203..a802c0ff97 100644
--- a/vector/src/main/res/xml/riotx_provider_paths.xml
+++ b/vector/src/main/res/xml/riotx_provider_paths.xml
@@ -3,4 +3,9 @@
     <cache-path
         name="shared"
         path="/" />
+
+    <external-path
+        name="external_files"
+        path="." />
+
 </paths>
\ No newline at end of file