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 {
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.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View File

@ -320,6 +320,7 @@ dependencies {
// File picker
implementation 'com.github.jaiselrahman:FilePicker:1.2.2'
implementation 'com.kbeanie:multipicker:1.6@aar'
// DI
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
import android.content.ActivityNotFoundException
import android.content.Context
import android.app.Activity
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 android.os.Bundle
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.*
import com.kbeanie.multipicker.api.CameraImagePicker
import com.kbeanie.multipicker.api.FilePicker
import com.kbeanie.multipicker.api.ImagePicker
import com.kbeanie.multipicker.api.Picker.*
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
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
fun selectFile(fragment: Fragment, requestCode: Int) {
selectMediaType(fragment, "*/*", null, requestCode)
interface Callback {
fun onAttachmentsReady(attachments: List<ContentAttachmentData>)
fun onAttachmentsProcessFailed()
}
fun selectGallery(fragment: Fragment, requestCode: Int) {
selectMediaType(fragment, "image/*", arrayOf("image/*", "video/*"), requestCode)
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
private val imagePicker by lazy {
ImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
it.allowMultiple()
}
}
fun openCamera(fragment: Fragment, requestCode: Int) {
dispatchTakePictureIntent(fragment, requestCode)
private val cameraImagePicker by lazy {
CameraImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
}
}
private val filePicker by lazy {
FilePicker(fragment).also {
it.allowMultiple()
it.setFilePickerCallback(attachmentsPickerCallback)
}
}
fun handleOpenCameraResult(): List<Attachment> {
val attachment = getAttachmentFromContentResolver(Uri.parse(capturePath))
return if (attachment == null) {
emptyList()
override fun onSaveInstanceState(outState: Bundle) {
capturePath?.also {
outState.putString(CAPTURE_PATH_KEY, it)
}
}
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 {
listOf(attachment)
val attachments = files.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
}
}
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)
}
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
if (images.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val uri = data?.data ?: return emptyList()
val attachment = getAttachmentFromContentResolver(uri)
return if (attachment == null) {
emptyList()
} else {
listOf(attachment)
val attachments = images.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
}
}
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
}
override fun onError(error: String?) {
callback.onAttachmentsProcessFailed()
}
}
@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 */
private fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
size = size,
date = createdAt.time,
name = displayName
)
// 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)
private fun ChosenFile.mapType(): ContentAttachmentData.Type {
return when {
mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE
mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO
mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
}
}
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
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.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 attachments: List<Attachment>) : RoomDetailActions()
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailActions()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : 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.loader.ImageLoader
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.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory
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.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.*
@ -125,10 +124,7 @@ data class RoomDetailArgs(
) : Parcelable
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
private const val REACTION_SELECT_REQUEST_CODE = 0
class RoomDetailFragment :
VectorBaseFragment(),
@ -136,7 +132,8 @@ class RoomDetailFragment :
AutocompleteUserPresenter.Callback,
VectorInviteView.Callback,
JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback {
AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback {
companion object {
@ -198,7 +195,15 @@ class RoomDetailFragment :
private lateinit var actionViewModel: ActionsHandler
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)
lateinit var composerLayout: TextComposerView
@ -213,7 +218,6 @@ 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()
@ -430,17 +434,9 @@ class RoomDetailFragment :
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_OPEN_CAMERA) {
val attachments = attachmentsHelper.handleOpenCameraResult()
roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
} else if (data != null) {
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
if (!hasBeenHandled && resultCode == RESULT_OK && 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
@ -452,9 +448,8 @@ class RoomDetailFragment :
}
}
}
}
// PRIVATE METHODS *****************************************************************************
// PRIVATE METHODS *****************************************************************************
private fun setupRecyclerView() {
@ -1086,7 +1081,7 @@ class RoomDetailFragment :
}
// VectorInviteView.Callback
// VectorInviteView.Callback
override fun onAcceptInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
@ -1098,7 +1093,7 @@ class RoomDetailFragment :
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
}
// JumpToReadMarkerView.Callback
// JumpToReadMarkerView.Callback
override fun onJumpToReadMarkerClicked(readMarkerId: String) {
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false))
@ -1108,14 +1103,25 @@ class RoomDetailFragment :
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
}
// AttachmentTypeSelectorView.Callback *********************************************************
// 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_CAMERA -> attachmentsHelper.openCamera()
AttachmentTypeSelectorView.TYPE_FILE -> attachmentsHelper.selectFile()
AttachmentTypeSelectorView.TYPE_GALLERY -> attachmentsHelper.selectGallery()
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) {
val attachments = action.attachments.map {
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 attachments = action.attachments
val homeServerCapabilities = session.getHomeServerCapabilities()
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {