Attachments: use a lib which handles for us all the intent stuff.

This commit is contained in:
ganfra 2019-10-09 19:51:00 +02:00
parent 3073470c38
commit 0a9ebb6bf6
7 changed files with 222 additions and 245 deletions

View File

@ -12,7 +12,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View File

@ -320,6 +320,7 @@ dependencies {
// File picker // File picker
implementation 'com.github.jaiselrahman:FilePicker:1.2.2' implementation 'com.github.jaiselrahman:FilePicker:1.2.2'
implementation 'com.kbeanie:multipicker:1.6@aar'
// DI // DI
implementation "com.google.dagger:dagger:$daggerVersion" implementation "com.google.dagger:dagger:$daggerVersion"

View File

@ -1,50 +0,0 @@
/*
* 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
}
}

View File

@ -15,138 +15,174 @@
*/ */
package im.vector.riotx.features.attachments package im.vector.riotx.features.attachments
import android.content.ActivityNotFoundException import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.BuildConfig import com.kbeanie.multipicker.api.CameraImagePicker
import im.vector.riotx.core.resources.MIME_TYPE_ALL_CONTENT import com.kbeanie.multipicker.api.FilePicker
import timber.log.Timber import com.kbeanie.multipicker.api.ImagePicker
import java.io.File import com.kbeanie.multipicker.api.Picker.*
import java.io.IOException import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
import java.text.SimpleDateFormat import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
import java.util.* 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.core.PickerManager
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.riotx.core.platform.Restorable
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
class AttachmentsHelper(private val context: Context) { class AttachmentsHelper(private val fragment: Fragment, private val callback: Callback) : Restorable {
private var capturePath: String? = null interface Callback {
fun onAttachmentsReady(attachments: List<ContentAttachmentData>)
fun selectFile(fragment: Fragment, requestCode: Int) { fun onAttachmentsProcessFailed()
selectMediaType(fragment, "*/*", null, requestCode)
} }
fun selectGallery(fragment: Fragment, requestCode: Int) { private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
selectMediaType(fragment, "image/*", arrayOf("image/*", "video/*"), requestCode)
private val imagePicker by lazy {
ImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
it.allowMultiple()
}
} }
fun openCamera(fragment: Fragment, requestCode: Int) { private val cameraImagePicker by lazy {
dispatchTakePictureIntent(fragment, requestCode) CameraImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
}
} }
private val filePicker by lazy {
FilePicker(fragment).also {
it.allowMultiple()
it.setFilePickerCallback(attachmentsPickerCallback)
}
}
fun handleOpenCameraResult(): List<Attachment> { override fun onSaveInstanceState(outState: Bundle) {
val attachment = getAttachmentFromContentResolver(Uri.parse(capturePath)) capturePath?.also {
return if (attachment == null) { outState.putString(CAPTURE_PATH_KEY, it)
emptyList() }
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY)
}
var capturePath: String? = null
private set
fun selectFile() {
filePicker.pickFile()
}
fun selectGallery() {
imagePicker.pickImage()
}
fun openCamera() {
capturePath = cameraImagePicker.pickImage()
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (resultCode == Activity.RESULT_OK) {
val pickerManager = getPickerManager(requestCode)
if (pickerManager != null) {
pickerManager.submit(data)
return true
}
}
return false
}
private fun getPickerManager(requestCode: Int): PickerManager? {
return when (requestCode) {
PICK_IMAGE_DEVICE -> imagePicker
PICK_IMAGE_CAMERA -> cameraImagePicker
PICK_FILE -> filePicker
else -> null
}
}
private inner class AttachmentsPickerCallback(private val callback: Callback) : ImagePickerCallback, FilePickerCallback {
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
if (files.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else { } else {
listOf(attachment) val attachments = files.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
} }
} }
fun handleSelectResult(data: Intent?): List<Attachment> { override fun onImagesChosen(images: MutableList<ChosenImage>?) {
val clipData = data?.clipData if (images.isNullOrEmpty()) {
if (clipData != null) { callback.onAttachmentsProcessFailed()
return (0 until clipData.itemCount).map {
clipData.getItemAt(it)
}.mapNotNull {
getAttachmentFromContentResolver(it.uri)
}
} else { } else {
val uri = data?.data ?: return emptyList() val attachments = images.map {
val attachment = getAttachmentFromContentResolver(uri) it.toContentAttachmentData()
return if (attachment == null) {
emptyList()
} else {
listOf(attachment)
} }
callback.onAttachmentsReady(attachments)
} }
} }
private fun selectMediaType(fragment: Fragment, type: String, extraMimeType: Array<String>?, requestCode: Int) { override fun onError(error: String?) {
val intent = Intent() callback.onAttachmentsProcessFailed()
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 ChosenFile.toContentAttachmentData(): ContentAttachmentData {
private fun createImageFile(context: Context): File { return ContentAttachmentData(
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) path = originalPath,
val imageFileName = "JPEG_" + timeStamp + "_" mimeType = mimeType,
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) type = mapType(),
val image = File.createTempFile( size = size,
imageFileName, /* prefix */ date = createdAt.time,
".jpg", /* suffix */ name = displayName
storageDir /* directory */
) )
// Save a file: path for use with ACTION_VIEW intents
capturePath = image.absolutePath
return image
} }
private fun dispatchTakePictureIntent(fragment: Fragment, requestCode: Int) { private fun ChosenFile.mapType(): ContentAttachmentData.Type {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) return when {
// Ensure that there's a camera activity to handle the intent mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE
if (takePictureIntent.resolveActivity(fragment.requireActivity().packageManager) != null) { mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO
// Create the File where the photo should go mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO
var photoFile: File? = null else -> ContentAttachmentData.Type.FILE
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)
}
} }
} }
private fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
name = displayName,
size = size,
height = height.toLong(),
width = width.toLong(),
date = createdAt.time
)
}
private fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = ContentAttachmentData.Type.VIDEO,
size = size,
date = createdAt.time,
height = height.toLong(),
width = width.toLong(),
duration = duration,
name = displayName
)
}
} }

View File

@ -16,17 +16,17 @@
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.home.room.detail
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event 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.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.features.attachments.Attachment
sealed class RoomDetailActions { sealed class RoomDetailActions {
data class SaveDraft(val draft: String) : RoomDetailActions() data class SaveDraft(val draft: String) : RoomDetailActions()
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val attachments: List<Attachment>) : RoomDetailActions() data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailActions()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()

View File

@ -49,13 +49,12 @@ import com.airbnb.mvrx.*
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.jaiselrahman.filepicker.activity.FilePickerActivity
import com.jaiselrahman.filepicker.model.MediaFile
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
@ -125,10 +124,7 @@ data class RoomDetailArgs(
) : Parcelable ) : Parcelable
private const val REQUEST_CODE_SELECT_FILE = 1 private const val REACTION_SELECT_REQUEST_CODE = 0
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 : class RoomDetailFragment :
VectorBaseFragment(), VectorBaseFragment(),
@ -136,7 +132,8 @@ class RoomDetailFragment :
AutocompleteUserPresenter.Callback, AutocompleteUserPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback { AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback {
companion object { companion object {
@ -198,7 +195,15 @@ class RoomDetailFragment :
private lateinit var actionViewModel: ActionsHandler private lateinit var actionViewModel: ActionsHandler
private lateinit var layoutManager: LinearLayoutManager private lateinit var layoutManager: LinearLayoutManager
private lateinit var attachmentsHelper: AttachmentsHelper
private lateinit var _attachmentsHelper: AttachmentsHelper
private val attachmentsHelper: AttachmentsHelper
get() {
if (::_attachmentsHelper.isInitialized.not()) {
_attachmentsHelper = AttachmentsHelper(this, this).register()
}
return _attachmentsHelper
}
@BindView(R.id.composerLayout) @BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView lateinit var composerLayout: TextComposerView
@ -213,7 +218,6 @@ class RoomDetailFragment :
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
attachmentsHelper = AttachmentsHelper((requireActivity()))
setupToolbar(roomToolbar) setupToolbar(roomToolbar)
setupRecyclerView() setupRecyclerView()
setupComposer() setupComposer()
@ -430,17 +434,9 @@ class RoomDetailFragment :
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == RESULT_OK) { val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_OPEN_CAMERA) { if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
val attachments = attachmentsHelper.handleOpenCameraResult()
roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
} else if (data != null) {
when (requestCode) { when (requestCode) {
REQUEST_CODE_SELECT_FILE,
REQUEST_CODE_SELECT_GALLERY -> {
val attachments = attachmentsHelper.handleSelectResult(data)
roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
}
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return ?: return
@ -452,7 +448,6 @@ class RoomDetailFragment :
} }
} }
} }
}
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
@ -1108,14 +1103,25 @@ class RoomDetailFragment :
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
} }
// AttachmentTypeSelectorView.Callback ********************************************************* // AttachmentTypeSelectorView.Callback
override fun onTypeSelected(type: Int) { override fun onTypeSelected(type: Int) {
when (type) { when (type) {
AttachmentTypeSelectorView.TYPE_CAMERA -> attachmentsHelper.openCamera(this, REQUEST_CODE_OPEN_CAMERA) AttachmentTypeSelectorView.TYPE_CAMERA -> attachmentsHelper.openCamera()
AttachmentTypeSelectorView.TYPE_FILE -> attachmentsHelper.selectFile(this, REQUEST_CODE_SELECT_FILE) AttachmentTypeSelectorView.TYPE_FILE -> attachmentsHelper.selectFile()
AttachmentTypeSelectorView.TYPE_GALLERY -> attachmentsHelper.selectGallery(this, REQUEST_CODE_SELECT_GALLERY) AttachmentTypeSelectorView.TYPE_GALLERY -> attachmentsHelper.selectGallery()
AttachmentTypeSelectorView.TYPE_STICKER -> vectorBaseActivity.notImplemented("Adding stickers") AttachmentTypeSelectorView.TYPE_STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
} }
} }
// AttachmentsHelper.Callback
override fun onAttachmentsReady(attachments: List<ContentAttachmentData>) {
Timber.v("onAttachmentsReady")
roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
}
override fun onAttachmentsProcessFailed() {
Timber.v("onAttachmentsProcessFailed")
}
} }

View File

@ -466,24 +466,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleSendMedia(action: RoomDetailActions.SendMedia) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.attachments.map { val attachments = action.attachments
val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path))
ContentAttachmentData(
size = it.size,
duration = it.duration,
date = it.date,
height = it.height,
width = it.width,
name = nameWithExtension ?: it.name,
path = it.path,
mimeType = it.mimeType,
type = ContentAttachmentData.Type.values()[it.type]
)
}
val homeServerCapabilities = session.getHomeServerCapabilities() val homeServerCapabilities = session.getHomeServerCapabilities()
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) { if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {