mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-28 22:18:46 +03:00
Merge pull request #1155 from vector-im/feature/multipicker
Multiple attachment picker implementation
This commit is contained in:
commit
3bb5e127d6
43 changed files with 1392 additions and 524 deletions
|
@ -119,7 +119,7 @@ dependencies {
|
||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
|
|
||||||
// Image
|
// Image
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
|
||||||
implementation 'id.zelory:compressor:3.0.0'
|
implementation 'id.zelory:compressor:3.0.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.content
|
package im.vector.matrix.android.api.session.content
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
@ -29,8 +30,7 @@ data class ContentAttachmentData(
|
||||||
val width: Long? = 0,
|
val width: Long? = 0,
|
||||||
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val queryUri: String,
|
val queryUri: Uri,
|
||||||
val path: String,
|
|
||||||
private val mimeType: String?,
|
private val mimeType: String?,
|
||||||
val type: Type
|
val type: Type
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
|
@ -53,9 +53,9 @@ internal class FileUploader @Inject constructor(@Authenticated
|
||||||
|
|
||||||
suspend fun uploadByteArray(byteArray: ByteArray,
|
suspend fun uploadByteArray(byteArray: ByteArray,
|
||||||
filename: String?,
|
filename: String?,
|
||||||
mimeType: String,
|
mimeType: String?,
|
||||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||||
val uploadBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull())
|
val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull())
|
||||||
return upload(uploadBody, filename, progressListener)
|
return upload(uploadBody, filename, progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.ThumbnailUtils
|
import android.media.MediaMetadataRetriever
|
||||||
import android.provider.MediaStore
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
internal object ThumbnailExtractor {
|
internal object ThumbnailExtractor {
|
||||||
|
|
||||||
|
@ -33,34 +33,40 @@ internal object ThumbnailExtractor {
|
||||||
val mimeType: String
|
val mimeType: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
|
fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? {
|
||||||
val file = File(attachment.path)
|
|
||||||
if (!file.exists() || !file.isFile) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return if (attachment.type == ContentAttachmentData.Type.VIDEO) {
|
return if (attachment.type == ContentAttachmentData.Type.VIDEO) {
|
||||||
extractVideoThumbnail(attachment)
|
extractVideoThumbnail(context, attachment)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
|
private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? {
|
||||||
val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) ?: return null
|
var thumbnailData: ThumbnailData? = null
|
||||||
val outputStream = ByteArrayOutputStream()
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
try {
|
||||||
val thumbnailWidth = thumbnail.width
|
mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
|
||||||
val thumbnailHeight = thumbnail.height
|
val thumbnail = mediaMetadataRetriever.frameAtTime
|
||||||
val thumbnailSize = outputStream.size()
|
|
||||||
val thumbnailData = ThumbnailData(
|
val outputStream = ByteArrayOutputStream()
|
||||||
width = thumbnailWidth,
|
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
height = thumbnailHeight,
|
val thumbnailWidth = thumbnail.width
|
||||||
size = thumbnailSize.toLong(),
|
val thumbnailHeight = thumbnail.height
|
||||||
bytes = outputStream.toByteArray(),
|
val thumbnailSize = outputStream.size()
|
||||||
mimeType = "image/jpeg"
|
thumbnailData = ThumbnailData(
|
||||||
)
|
width = thumbnailWidth,
|
||||||
thumbnail.recycle()
|
height = thumbnailHeight,
|
||||||
outputStream.reset()
|
size = thumbnailSize.toLong(),
|
||||||
|
bytes = outputStream.toByteArray(),
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
)
|
||||||
|
thumbnail.recycle()
|
||||||
|
outputStream.reset()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Cannot extract video thumbnail")
|
||||||
|
} finally {
|
||||||
|
mediaMetadataRetriever.release()
|
||||||
|
}
|
||||||
return thumbnailData
|
return thumbnailData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,9 @@
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import id.zelory.compressor.Compressor
|
|
||||||
import id.zelory.compressor.constraint.default
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
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.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
@ -41,8 +38,6 @@ import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private data class NewImageAttributes(
|
private data class NewImageAttributes(
|
||||||
|
@ -94,8 +89,90 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
|
|
||||||
var newImageAttributes: NewImageAttributes? = null
|
var newImageAttributes: NewImageAttributes? = null
|
||||||
|
|
||||||
val attachmentFile = try {
|
try {
|
||||||
File(attachment.path)
|
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
|
||||||
|
?: return Result.success(
|
||||||
|
WorkerParamsFactory.toData(
|
||||||
|
params.copy(
|
||||||
|
lastFailureMessage = "Cannot openInputStream for file: " + attachment.queryUri.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
inputStream.use {
|
||||||
|
var uploadedThumbnailUrl: String? = null
|
||||||
|
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
|
ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData ->
|
||||||
|
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
||||||
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
|
Timber.v("Encrypt thumbnail")
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||||
|
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||||
|
"thumb_${attachment.name}",
|
||||||
|
"application/octet-stream",
|
||||||
|
thumbnailProgressListener)
|
||||||
|
} else {
|
||||||
|
fileUploader.uploadByteArray(thumbnailData.bytes,
|
||||||
|
"thumb_${attachment.name}",
|
||||||
|
thumbnailData.mimeType,
|
||||||
|
thumbnailProgressListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t, "Thumbnail update failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressListener = object : ProgressRequestBody.Listener {
|
||||||
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
notifyTracker(params) {
|
||||||
|
if (isStopped) {
|
||||||
|
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
||||||
|
} else {
|
||||||
|
contentUploadStateTracker.setProgress(it, current, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
|
Timber.v("Encrypt file")
|
||||||
|
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
||||||
|
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(inputStream, attachment.getSafeMimeType())
|
||||||
|
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
||||||
|
} else {
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(inputStream.readBytes(), attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSuccess(params,
|
||||||
|
contentUploadResponse.contentUri,
|
||||||
|
uploadedFileEncryptedFileInfo,
|
||||||
|
uploadedThumbnailUrl,
|
||||||
|
uploadedThumbnailEncryptedFileInfo,
|
||||||
|
newImageAttributes)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t)
|
||||||
|
handleFailure(params, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||||
|
@ -106,109 +183,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
.let { originalFile ->
|
|
||||||
if (attachment.type == ContentAttachmentData.Type.IMAGE) {
|
|
||||||
if (params.compressBeforeSending) {
|
|
||||||
Compressor.compress(context, originalFile) {
|
|
||||||
default(
|
|
||||||
width = MAX_IMAGE_SIZE,
|
|
||||||
height = MAX_IMAGE_SIZE
|
|
||||||
)
|
|
||||||
}.also { compressedFile ->
|
|
||||||
// Update the params
|
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
||||||
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
|
|
||||||
val fileSize = compressedFile.length().toInt()
|
|
||||||
|
|
||||||
newImageAttributes = NewImageAttributes(
|
|
||||||
options.outWidth,
|
|
||||||
options.outHeight,
|
|
||||||
fileSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO Fix here the image rotation issue
|
|
||||||
originalFile
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other type
|
|
||||||
originalFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadedThumbnailUrl: String? = null
|
|
||||||
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
|
||||||
|
|
||||||
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
|
|
||||||
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
|
||||||
override fun onProgress(current: Long, total: Long) {
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val contentUploadResponse = if (params.isEncrypted) {
|
|
||||||
Timber.v("Encrypt thumbnail")
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
|
||||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
|
||||||
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
|
||||||
"thumb_${attachment.name}",
|
|
||||||
"application/octet-stream",
|
|
||||||
thumbnailProgressListener)
|
|
||||||
} else {
|
|
||||||
fileUploader.uploadByteArray(thumbnailData.bytes,
|
|
||||||
"thumb_${attachment.name}",
|
|
||||||
thumbnailData.mimeType,
|
|
||||||
thumbnailProgressListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Timber.e(t)
|
|
||||||
return handleFailure(params, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val progressListener = object : ProgressRequestBody.Listener {
|
|
||||||
override fun onProgress(current: Long, total: Long) {
|
|
||||||
notifyTracker(params) {
|
|
||||||
if (isStopped) {
|
|
||||||
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
|
||||||
} else {
|
|
||||||
contentUploadStateTracker.setProgress(it, current, total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val contentUploadResponse = if (params.isEncrypted) {
|
|
||||||
Timber.v("Encrypt file")
|
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
|
||||||
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType())
|
|
||||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
|
||||||
|
|
||||||
fileUploader
|
|
||||||
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
|
||||||
} else {
|
|
||||||
fileUploader
|
|
||||||
.uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSuccess(params,
|
|
||||||
contentUploadResponse.contentUri,
|
|
||||||
uploadedFileEncryptedFileInfo,
|
|
||||||
uploadedThumbnailUrl,
|
|
||||||
uploadedThumbnailEncryptedFileInfo,
|
|
||||||
newImageAttributes)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Timber.e(t)
|
|
||||||
handleFailure(params, t)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
@ -74,6 +75,7 @@ import javax.inject.Inject
|
||||||
* The transactionId is used as loc
|
* The transactionId is used as loc
|
||||||
*/
|
*/
|
||||||
internal class LocalEchoEventFactory @Inject constructor(
|
internal class LocalEchoEventFactory @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val textPillsUtils: TextPillsUtils,
|
private val textPillsUtils: TextPillsUtils,
|
||||||
|
@ -266,14 +268,14 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
height = height?.toInt() ?: 0,
|
height = height?.toInt() ?: 0,
|
||||||
size = attachment.size.toInt()
|
size = attachment.size.toInt()
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||||
val mediaDataRetriever = MediaMetadataRetriever()
|
val mediaDataRetriever = MediaMetadataRetriever()
|
||||||
mediaDataRetriever.setDataSource(attachment.path)
|
mediaDataRetriever.setDataSource(context, attachment.queryUri)
|
||||||
|
|
||||||
// Use frame to calculate height and width as we are sure to get the right ones
|
// Use frame to calculate height and width as we are sure to get the right ones
|
||||||
val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime
|
val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime
|
||||||
|
@ -281,7 +283,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
val width = firstFrame?.width ?: 0
|
val width = firstFrame?.width ?: 0
|
||||||
mediaDataRetriever.release()
|
mediaDataRetriever.release()
|
||||||
|
|
||||||
val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let {
|
val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let {
|
||||||
ThumbnailInfo(
|
ThumbnailInfo(
|
||||||
width = it.width,
|
width = it.width,
|
||||||
height = it.height,
|
height = it.height,
|
||||||
|
@ -299,10 +301,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
size = attachment.size,
|
size = attachment.size,
|
||||||
duration = attachment.duration?.toInt() ?: 0,
|
duration = attachment.duration?.toInt() ?: 0,
|
||||||
// Glide will be able to use the local path and extract a thumbnail.
|
// Glide will be able to use the local path and extract a thumbnail.
|
||||||
thumbnailUrl = attachment.path,
|
thumbnailUrl = attachment.queryUri.toString(),
|
||||||
thumbnailInfo = thumbnailInfo
|
thumbnailInfo = thumbnailInfo
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
@ -315,7 +317,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
@ -329,7 +331,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
?: "application/octet-stream",
|
?: "application/octet-stream",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.queryUri.toString()
|
||||||
)
|
)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
1
multipicker/.gitignore
vendored
Normal file
1
multipicker/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
56
multipicker/build.gradle
Normal file
56
multipicker/build.gradle
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 29
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion 29
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles 'consumer-rules.pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
implementation 'androidx.core:core-ktx:1.2.0'
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
|
|
||||||
|
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
|
||||||
|
|
||||||
|
// Log
|
||||||
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
}
|
0
multipicker/consumer-rules.pro
Normal file
0
multipicker/consumer-rules.pro
Normal file
21
multipicker/proguard-rules.pro
vendored
Normal file
21
multipicker/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
16
multipicker/src/main/AndroidManifest.xml
Normal file
16
multipicker/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="im.vector.riotx.multipicker">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<provider
|
||||||
|
android:name=".provider.MultiPickerFileProvider"
|
||||||
|
android:authorities="${applicationId}.multipicker.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/multipicker_provider_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerAudioType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio file picker implementation
|
||||||
|
*/
|
||||||
|
class AudioPicker(override val requestCode: Int) : Picker<MultiPickerAudioType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected audio files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerAudioType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioList = mutableListOf<MultiPickerAudioType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Audio.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
var duration = 0L
|
||||||
|
|
||||||
|
context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd ->
|
||||||
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
|
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
|
||||||
|
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
audioList.add(
|
||||||
|
MultiPickerAudioType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return audioList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "audio/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||||
|
import im.vector.riotx.multipicker.utils.ImageUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of taking a photo with Camera
|
||||||
|
*/
|
||||||
|
class CameraPicker(val requestCode: Int) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start camera by using an Activity
|
||||||
|
* @param activity Activity to handle onActivityResult().
|
||||||
|
* @return Uri of taken photo or null if the operation is cancelled.
|
||||||
|
*/
|
||||||
|
fun startWithExpectingFile(activity: Activity): Uri? {
|
||||||
|
val photoUri = createPhotoUri(activity)
|
||||||
|
val intent = createIntent().apply {
|
||||||
|
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
|
||||||
|
}
|
||||||
|
activity.startActivityForResult(intent, requestCode)
|
||||||
|
return photoUri
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start camera by using a Fragment
|
||||||
|
* @param fragment Fragment to handle onActivityResult().
|
||||||
|
* @return Uri of taken photo or null if the operation is cancelled.
|
||||||
|
*/
|
||||||
|
fun startWithExpectingFile(fragment: Fragment): Uri? {
|
||||||
|
val photoUri = createPhotoUri(fragment.requireContext())
|
||||||
|
val intent = createIntent().apply {
|
||||||
|
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
|
||||||
|
}
|
||||||
|
fragment.startActivityForResult(intent, requestCode)
|
||||||
|
return photoUri
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* @return Taken photo or null if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user cancelled the operation.
|
||||||
|
*/
|
||||||
|
fun getTakenPhoto(context: Context, requestCode: Int, resultCode: Int, photoUri: Uri): MultiPickerImageType? {
|
||||||
|
if (requestCode == this.requestCode && resultCode == Activity.RESULT_OK) {
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Images.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
photoUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
|
||||||
|
val bitmap = ImageUtils.getBitmap(context, photoUri)
|
||||||
|
val orientation = ImageUtils.getOrientation(context, photoUri)
|
||||||
|
|
||||||
|
return MultiPickerImageType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(photoUri),
|
||||||
|
photoUri,
|
||||||
|
bitmap?.width ?: 0,
|
||||||
|
bitmap?.height ?: 0,
|
||||||
|
orientation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIntent(): Intent {
|
||||||
|
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPhotoUri(context: Context): Uri {
|
||||||
|
val file = createImageFile(context)
|
||||||
|
val authority = context.packageName + ".multipicker.fileprovider"
|
||||||
|
return FileProvider.getUriForFile(context, authority, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createImageFile(context: Context): File {
|
||||||
|
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val storageDir: File = context.filesDir
|
||||||
|
return File.createTempFile(
|
||||||
|
"${timeStamp}_", /* prefix */
|
||||||
|
".jpg", /* suffix */
|
||||||
|
storageDir /* directory */
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerContactType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact Picker implementation
|
||||||
|
*/
|
||||||
|
class ContactPicker(override val requestCode: Int) : Picker<MultiPickerContactType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected contact or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerContactType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val contactList = mutableListOf<MultiPickerContactType>()
|
||||||
|
|
||||||
|
data?.data?.let { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
ContactsContract.Contacts.DISPLAY_NAME,
|
||||||
|
ContactsContract.Contacts.PHOTO_URI,
|
||||||
|
ContactsContract.Contacts._ID
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID)
|
||||||
|
val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
|
||||||
|
val photoUriColumn = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI)
|
||||||
|
|
||||||
|
val contactId = cursor.getInt(idColumn)
|
||||||
|
var name = cursor.getString(nameColumn)
|
||||||
|
var photoUri = cursor.getString(photoUriColumn)
|
||||||
|
var phoneNumberList = mutableListOf<String>()
|
||||||
|
var emailList = mutableListOf<String>()
|
||||||
|
|
||||||
|
getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
|
||||||
|
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"
|
||||||
|
val selectionArgs = arrayOf(rawContactId.toString())
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
ContactsContract.Data.CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
ContactsContract.Data.MIMETYPE,
|
||||||
|
ContactsContract.Data.DATA1
|
||||||
|
),
|
||||||
|
selection,
|
||||||
|
selectionArgs,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))
|
||||||
|
val contactData = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1))
|
||||||
|
|
||||||
|
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
||||||
|
name = contactData
|
||||||
|
}
|
||||||
|
if (mimeType == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) {
|
||||||
|
phoneNumberList.add(contactData)
|
||||||
|
}
|
||||||
|
if (mimeType == ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) {
|
||||||
|
emailList.add(contactData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contactList.add(
|
||||||
|
MultiPickerContactType(
|
||||||
|
name,
|
||||||
|
photoUri,
|
||||||
|
phoneNumberList,
|
||||||
|
emailList
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contactList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRawContactId(contentResolver: ContentResolver, contactId: Int): Int? {
|
||||||
|
val projection = arrayOf(ContactsContract.RawContacts._ID)
|
||||||
|
val selection = ContactsContract.RawContacts.CONTACT_ID + " = ?"
|
||||||
|
val selectionArgs = arrayOf(contactId.toString() + "")
|
||||||
|
return contentResolver.query(
|
||||||
|
ContactsContract.RawContacts.CONTENT_URI,
|
||||||
|
projection,
|
||||||
|
selection,
|
||||||
|
selectionArgs,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
return if (cursor.moveToFirst()) cursor.getInt(cursor.getColumnIndex(ContactsContract.RawContacts._ID)) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_PICK).apply {
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = ContactsContract.Contacts.CONTENT_TYPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerFileType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of selecting any type of files
|
||||||
|
*/
|
||||||
|
class FilePicker(override val requestCode: Int) : Picker<MultiPickerFileType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerFileType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileList = mutableListOf<MultiPickerFileType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
context.contentResolver.query(selectedUri, null, null, null, null)
|
||||||
|
?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
|
||||||
|
fileList.add(
|
||||||
|
MultiPickerFileType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "*/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||||
|
import im.vector.riotx.multipicker.utils.ImageUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image Picker implementation
|
||||||
|
*/
|
||||||
|
class ImagePicker(override val requestCode: Int) : Picker<MultiPickerImageType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected image files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerImageType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageList = mutableListOf<MultiPickerImageType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Images.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
|
||||||
|
val bitmap = ImageUtils.getBitmap(context, selectedUri)
|
||||||
|
val orientation = ImageUtils.getOrientation(context, selectedUri)
|
||||||
|
|
||||||
|
imageList.add(
|
||||||
|
MultiPickerImageType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri,
|
||||||
|
bitmap?.width ?: 0,
|
||||||
|
bitmap?.height ?: 0,
|
||||||
|
orientation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "image/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
class MultiPicker<T> {
|
||||||
|
|
||||||
|
companion object Type {
|
||||||
|
val IMAGE by lazy { MultiPicker<ImagePicker>() }
|
||||||
|
val FILE by lazy { MultiPicker<FilePicker>() }
|
||||||
|
val VIDEO by lazy { MultiPicker<VideoPicker>() }
|
||||||
|
val AUDIO by lazy { MultiPicker<AudioPicker>() }
|
||||||
|
val CONTACT by lazy { MultiPicker<ContactPicker>() }
|
||||||
|
val CAMERA by lazy { MultiPicker<CameraPicker>() }
|
||||||
|
|
||||||
|
const val REQUEST_CODE_PICK_IMAGE = 5000
|
||||||
|
const val REQUEST_CODE_PICK_VIDEO = 5001
|
||||||
|
const val REQUEST_CODE_PICK_FILE = 5002
|
||||||
|
const val REQUEST_CODE_PICK_AUDIO = 5003
|
||||||
|
const val REQUEST_CODE_PICK_CONTACT = 5004
|
||||||
|
const val REQUEST_CODE_TAKE_PHOTO = 5005
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T> get(type: MultiPicker<T>): T {
|
||||||
|
return when (type) {
|
||||||
|
IMAGE -> ImagePicker(REQUEST_CODE_PICK_IMAGE) as T
|
||||||
|
VIDEO -> VideoPicker(REQUEST_CODE_PICK_VIDEO) as T
|
||||||
|
FILE -> FilePicker(REQUEST_CODE_PICK_FILE) as T
|
||||||
|
AUDIO -> AudioPicker(REQUEST_CODE_PICK_AUDIO) as T
|
||||||
|
CONTACT -> ContactPicker(REQUEST_CODE_PICK_CONTACT) as T
|
||||||
|
CAMERA -> CameraPicker(REQUEST_CODE_TAKE_PHOTO) as T
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt
Normal file
116
multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class to provide all types of Pickers
|
||||||
|
*/
|
||||||
|
abstract class Picker<T>(open val requestCode: Int) {
|
||||||
|
|
||||||
|
protected var single = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* @return selected files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
abstract fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to retrieve files which are shared from another application or internally
|
||||||
|
* by using android.intent.action.SEND or android.intent.action.SEND_MULTIPLE actions.
|
||||||
|
*/
|
||||||
|
fun getIncomingFiles(context: Context, data: Intent?): List<T> {
|
||||||
|
if (data == null) return emptyList()
|
||||||
|
|
||||||
|
val uriList = mutableListOf<Uri>()
|
||||||
|
if (data.action == Intent.ACTION_SEND) {
|
||||||
|
(data.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { uriList.add(it) }
|
||||||
|
} else if (data.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||||
|
val extraUriList: List<Uri>? = data.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
|
||||||
|
extraUriList?.let { uriList.addAll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivities(data, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
uriList.forEach {
|
||||||
|
for (resolveInfo in resInfoList) {
|
||||||
|
val packageName: String = resolveInfo.activityInfo.packageName
|
||||||
|
context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getSelectedFiles(context, requestCode, Activity.RESULT_OK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function to disable multiple selection of files.
|
||||||
|
*/
|
||||||
|
fun single(): Picker<T> {
|
||||||
|
single = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun createIntent(): Intent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Storage Access Framework UI by using an Activity.
|
||||||
|
* @param activity Activity to handle onActivityResult().
|
||||||
|
*/
|
||||||
|
fun startWith(activity: Activity) {
|
||||||
|
activity.startActivityForResult(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, requestCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Storage Access Framework UI by using a Fragment.
|
||||||
|
* @param fragment Fragment to handle onActivityResult().
|
||||||
|
*/
|
||||||
|
fun startWith(fragment: Fragment) {
|
||||||
|
fragment.startActivityForResult(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, requestCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getSelectedUriList(data: Intent?): List<Uri> {
|
||||||
|
val selectedUriList = mutableListOf<Uri>()
|
||||||
|
val dataUri = data?.data
|
||||||
|
val clipData = data?.clipData
|
||||||
|
|
||||||
|
if (clipData != null) {
|
||||||
|
for (i in 0 until clipData.itemCount) {
|
||||||
|
selectedUriList.add(clipData.getItemAt(i).uri)
|
||||||
|
}
|
||||||
|
} else if (dataUri != null) {
|
||||||
|
selectedUriList.add(dataUri)
|
||||||
|
} else {
|
||||||
|
data?.extras?.get(Intent.EXTRA_STREAM)?.let {
|
||||||
|
(it as? List<*>)?.filterIsInstance<Uri>()?.let { uriList ->
|
||||||
|
selectedUriList.addAll(uriList)
|
||||||
|
}
|
||||||
|
if (it is Uri) {
|
||||||
|
selectedUriList.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedUriList
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerVideoType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video Picker implementation
|
||||||
|
*/
|
||||||
|
class VideoPicker(override val requestCode: Int) : Picker<MultiPickerVideoType>(requestCode) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function from onActivityResult(int, int, Intent).
|
||||||
|
* Returns selected video files or empty list if request code is wrong
|
||||||
|
* or result code is not Activity.RESULT_OK
|
||||||
|
* or user did not select any files.
|
||||||
|
*/
|
||||||
|
override fun getSelectedFiles(context: Context, requestCode: Int, resultCode: Int, data: Intent?): List<MultiPickerVideoType> {
|
||||||
|
if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val videoList = mutableListOf<MultiPickerVideoType>()
|
||||||
|
|
||||||
|
getSelectedUriList(data).forEach { selectedUri ->
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Video.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Video.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
selectedUri,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME)
|
||||||
|
val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE)
|
||||||
|
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
val name = cursor.getString(nameColumn)
|
||||||
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
var duration = 0L
|
||||||
|
var width = 0
|
||||||
|
var height = 0
|
||||||
|
var orientation = 0
|
||||||
|
|
||||||
|
context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd ->
|
||||||
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
|
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
|
||||||
|
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
|
||||||
|
width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH).toInt()
|
||||||
|
height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT).toInt()
|
||||||
|
orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
videoList.add(
|
||||||
|
MultiPickerVideoType(
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
context.contentResolver.getType(selectedUri),
|
||||||
|
selectedUri,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
orientation,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
|
type = "video/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerAudioType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri,
|
||||||
|
val duration: Long
|
||||||
|
) : MultiPickerBaseType
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
interface MultiPickerBaseType {
|
||||||
|
val displayName: String?
|
||||||
|
val size: Long
|
||||||
|
val mimeType: String?
|
||||||
|
val contentUri: Uri
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.entity
|
||||||
|
|
||||||
|
data class MultiPickerContactType(
|
||||||
|
val displayName: String,
|
||||||
|
val photoUri: String?,
|
||||||
|
val phoneNumberList: List<String>,
|
||||||
|
val emailList: List<String>
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerFileType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri
|
||||||
|
) : MultiPickerBaseType
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerImageType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val orientation: Int
|
||||||
|
) : MultiPickerBaseType
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.entity
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class MultiPickerVideoType(
|
||||||
|
override val displayName: String?,
|
||||||
|
override val size: Long,
|
||||||
|
override val mimeType: String?,
|
||||||
|
override val contentUri: Uri,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val orientation: Int,
|
||||||
|
val duration: Long
|
||||||
|
) : MultiPickerBaseType
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.provider
|
||||||
|
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
|
||||||
|
class MultiPickerFileProvider : FileProvider()
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.multipicker.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object ImageUtils {
|
||||||
|
|
||||||
|
fun getBitmap(context: Context, uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri))
|
||||||
|
} else {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Cannot decode Bitmap: %s", uri.toString())
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrientation(context: Context, uri: Uri): Int {
|
||||||
|
var orientation = 0
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
try {
|
||||||
|
ExifInterface(inputStream).let {
|
||||||
|
orientation = it.rotationDegrees
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Cannot read orientation: %s", uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orientation
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<files-path
|
||||||
|
name="external_files"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
|
@ -1 +1,2 @@
|
||||||
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
|
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
|
||||||
|
include ':multipicker'
|
||||||
|
|
|
@ -254,6 +254,7 @@ dependencies {
|
||||||
implementation project(":matrix-sdk-android")
|
implementation project(":matrix-sdk-android")
|
||||||
implementation project(":matrix-sdk-android-rx")
|
implementation project(":matrix-sdk-android-rx")
|
||||||
implementation project(":diff-match-patch")
|
implementation project(":diff-match-patch")
|
||||||
|
implementation project(":multipicker")
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
@ -347,9 +348,6 @@ dependencies {
|
||||||
// Badge for compatibility
|
// Badge for compatibility
|
||||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||||
|
|
||||||
// File picker
|
|
||||||
implementation 'com.kbeanie:multipicker:1.6@aar'
|
|
||||||
|
|
||||||
// DI
|
// DI
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||||
|
|
|
@ -18,20 +18,13 @@ package im.vector.riotx.features.attachments
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_AUDIO
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_CONTACT
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_FILE
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA
|
|
||||||
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE
|
|
||||||
import com.kbeanie.multipicker.core.ImagePickerImpl
|
|
||||||
import com.kbeanie.multipicker.core.PickerManager
|
|
||||||
import com.kbeanie.multipicker.utils.IntentUtils
|
|
||||||
import im.vector.matrix.android.BuildConfig
|
import im.vector.matrix.android.BuildConfig
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.riotx.core.platform.Restorable
|
import im.vector.riotx.core.platform.Restorable
|
||||||
import im.vector.riotx.features.attachments.AttachmentsHelper.Callback
|
import im.vector.riotx.multipicker.MultiPicker
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
||||||
|
@ -39,20 +32,8 @@ private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class helps to handle attachments by providing simple methods.
|
* This class helps to handle attachments by providing simple methods.
|
||||||
* The process is asynchronous and you must implement [Callback] methods to get the data or a failure.
|
|
||||||
*/
|
*/
|
||||||
class AttachmentsHelper private constructor(private val context: Context,
|
class AttachmentsHelper(val context: Context, val callback: Callback) : Restorable {
|
||||||
private val pickerManagerFactory: PickerManagerFactory) : Restorable {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(fragment: Fragment, callback: Callback): AttachmentsHelper {
|
|
||||||
return AttachmentsHelper(fragment.requireContext(), FragmentPickerManagerFactory(fragment, callback))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create(activity: Activity, callback: Callback): AttachmentsHelper {
|
|
||||||
return AttachmentsHelper(activity, ActivityPickerManagerFactory(activity, callback))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
|
@ -66,39 +47,15 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture path allows to handle camera image picking. It must be restored if the activity gets killed.
|
// Capture path allows to handle camera image picking. It must be restored if the activity gets killed.
|
||||||
private var capturePath: String? = null
|
private var captureUri: Uri? = null
|
||||||
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
|
// 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
|
var pendingType: AttachmentTypeSelectorView.Type? = null
|
||||||
|
|
||||||
private val imagePicker by lazy {
|
|
||||||
pickerManagerFactory.createImagePicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val videoPicker by lazy {
|
|
||||||
pickerManagerFactory.createVideoPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cameraImagePicker by lazy {
|
|
||||||
pickerManagerFactory.createCameraImagePicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val filePicker by lazy {
|
|
||||||
pickerManagerFactory.createFilePicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val audioPicker by lazy {
|
|
||||||
pickerManagerFactory.createAudioPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val contactPicker by lazy {
|
|
||||||
pickerManagerFactory.createContactPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restorable
|
// Restorable
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
capturePath?.also {
|
captureUri?.also {
|
||||||
outState.putString(CAPTURE_PATH_KEY, it)
|
outState.putParcelable(CAPTURE_PATH_KEY, it)
|
||||||
}
|
}
|
||||||
pendingType?.also {
|
pendingType?.also {
|
||||||
outState.putSerializable(PENDING_TYPE_KEY, it)
|
outState.putSerializable(PENDING_TYPE_KEY, it)
|
||||||
|
@ -106,10 +63,7 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||||
capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY)
|
captureUri = savedInstanceState?.getParcelable(CAPTURE_PATH_KEY) as? Uri
|
||||||
if (capturePath != null) {
|
|
||||||
cameraImagePicker.reinitialize(capturePath)
|
|
||||||
}
|
|
||||||
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
|
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,36 +72,36 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling file picking
|
* Starts the process for handling file picking
|
||||||
*/
|
*/
|
||||||
fun selectFile() {
|
fun selectFile(fragment: Fragment) {
|
||||||
filePicker.pickFile()
|
MultiPicker.get(MultiPicker.FILE).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling image picking
|
* Starts the process for handling image picking
|
||||||
*/
|
*/
|
||||||
fun selectGallery() {
|
fun selectGallery(fragment: Fragment) {
|
||||||
imagePicker.pickImage()
|
MultiPicker.get(MultiPicker.IMAGE).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling audio picking
|
* Starts the process for handling audio picking
|
||||||
*/
|
*/
|
||||||
fun selectAudio() {
|
fun selectAudio(fragment: Fragment) {
|
||||||
audioPicker.pickAudio()
|
MultiPicker.get(MultiPicker.AUDIO).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling capture image picking
|
* Starts the process for handling capture image picking
|
||||||
*/
|
*/
|
||||||
fun openCamera() {
|
fun openCamera(fragment: Fragment) {
|
||||||
capturePath = cameraImagePicker.pickImage()
|
captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process for handling contact picking
|
* Starts the process for handling contact picking
|
||||||
*/
|
*/
|
||||||
fun selectContact() {
|
fun selectContact(fragment: Fragment) {
|
||||||
contactPicker.pickContact()
|
MultiPicker.get(MultiPicker.CONTACT).startWith(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,14 +111,58 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
*/
|
*/
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
val pickerManager = getPickerManagerForRequestCode(requestCode)
|
when (requestCode) {
|
||||||
if (pickerManager != null) {
|
MultiPicker.REQUEST_CODE_PICK_FILE -> {
|
||||||
if (pickerManager is ImagePickerImpl) {
|
callback.onContentAttachmentsReady(
|
||||||
pickerManager.reinitialize(capturePath)
|
MultiPicker.get(MultiPicker.FILE)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
pickerManager.submit(data)
|
MultiPicker.REQUEST_CODE_PICK_AUDIO -> {
|
||||||
return true
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.AUDIO)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_PICK_CONTACT -> {
|
||||||
|
MultiPicker.get(MultiPicker.CONTACT)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.firstOrNull()
|
||||||
|
?.toContactAttachment()
|
||||||
|
?.let {
|
||||||
|
callback.onContactAttachmentReady(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_PICK_IMAGE -> {
|
||||||
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.IMAGE)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_TAKE_PHOTO -> {
|
||||||
|
captureUri?.let { captureUri ->
|
||||||
|
MultiPicker.get(MultiPicker.CAMERA)
|
||||||
|
.getTakenPhoto(context, requestCode, resultCode, captureUri)
|
||||||
|
?.let {
|
||||||
|
callback.onContentAttachmentsReady(
|
||||||
|
listOf(it).map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MultiPicker.REQUEST_CODE_PICK_VIDEO -> {
|
||||||
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.VIDEO)
|
||||||
|
.getSelectedFiles(context, requestCode, resultCode, data)
|
||||||
|
.map { it.toContentAttachmentData() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -174,39 +172,35 @@ class AttachmentsHelper private constructor(private val context: Context,
|
||||||
*
|
*
|
||||||
* @return true if it can handle the intent data, false otherwise
|
* @return true if it can handle the intent data, false otherwise
|
||||||
*/
|
*/
|
||||||
fun handleShareIntent(intent: Intent): Boolean {
|
fun handleShareIntent(context: Context, intent: Intent): Boolean {
|
||||||
val type = intent.resolveType(context) ?: return false
|
val type = intent.resolveType(context) ?: return false
|
||||||
if (type.startsWith("image")) {
|
if (type.startsWith("image")) {
|
||||||
imagePicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.IMAGE).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else if (type.startsWith("video")) {
|
} else if (type.startsWith("video")) {
|
||||||
videoPicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else if (type.startsWith("audio")) {
|
} else if (type.startsWith("audio")) {
|
||||||
videoPicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.AUDIO).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
||||||
filePicker.submit(safeShareIntent(intent))
|
callback.onContentAttachmentsReady(
|
||||||
|
MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun safeShareIntent(intent: Intent): Intent {
|
|
||||||
// Work around for getPickerIntentForSharing doing NPE in android 10
|
|
||||||
return try {
|
|
||||||
IntentUtils.getPickerIntentForSharing(intent)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
intent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? {
|
|
||||||
return when (requestCode) {
|
|
||||||
PICK_IMAGE_DEVICE -> imagePicker
|
|
||||||
PICK_IMAGE_CAMERA -> cameraImagePicker
|
|
||||||
PICK_FILE -> filePicker
|
|
||||||
PICK_CONTACT -> contactPicker
|
|
||||||
PICK_AUDIO -> audioPicker
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,51 +16,48 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.attachments
|
package im.vector.riotx.features.attachments
|
||||||
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenAudio
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenContact
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenFile
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenImage
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenVideo
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerAudioType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerBaseType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerContactType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerFileType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||||
|
import im.vector.riotx.multipicker.entity.MultiPickerVideoType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
fun ChosenContact.toContactAttachment(): ContactAttachment {
|
fun MultiPickerContactType.toContactAttachment(): ContactAttachment {
|
||||||
return ContactAttachment(
|
return ContactAttachment(
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
photoUri = photoUri,
|
photoUri = photoUri,
|
||||||
emails = emails.toList(),
|
emails = emailList.toList(),
|
||||||
phones = phones.toList()
|
phones = phoneNumberList.toList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerFileType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = mapType(),
|
type = mapType(),
|
||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
|
||||||
name = displayName,
|
name = displayName,
|
||||||
queryUri = queryUri
|
queryUri = contentUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = mapType(),
|
type = mapType(),
|
||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
|
||||||
name = displayName,
|
name = displayName,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
queryUri = queryUri
|
queryUri = contentUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ChosenFile.mapType(): ContentAttachmentData.Type {
|
private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type {
|
||||||
return when {
|
return when {
|
||||||
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
|
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
|
||||||
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
|
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
|
||||||
|
@ -69,10 +66,9 @@ private fun ChosenFile.mapType(): ContentAttachmentData.Type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerImageType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = mapType(),
|
type = mapType(),
|
||||||
name = displayName,
|
name = displayName,
|
||||||
|
@ -80,23 +76,20 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
||||||
height = height.toLong(),
|
height = height.toLong(),
|
||||||
width = width.toLong(),
|
width = width.toLong(),
|
||||||
exifOrientation = orientation,
|
exifOrientation = orientation,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
queryUri = contentUri
|
||||||
queryUri = queryUri
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerVideoType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
path = originalPath,
|
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = ContentAttachmentData.Type.VIDEO,
|
type = ContentAttachmentData.Type.VIDEO,
|
||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
|
||||||
height = height.toLong(),
|
height = height.toLong(),
|
||||||
width = width.toLong(),
|
width = width.toLong(),
|
||||||
duration = duration,
|
duration = duration,
|
||||||
name = displayName,
|
name = displayName,
|
||||||
queryUri = queryUri
|
queryUri = contentUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +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 com.kbeanie.multipicker.api.callbacks.AudioPickerCallback
|
|
||||||
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.ChosenContact
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenFile
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenImage
|
|
||||||
import com.kbeanie.multipicker.api.entity.ChosenVideo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
|
|
||||||
*/
|
|
||||||
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()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = audios.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
|
|
||||||
if (files.isNullOrEmpty()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = files.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
|
|
||||||
if (images.isNullOrEmpty()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = images.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVideosChosen(videos: MutableList<ChosenVideo>?) {
|
|
||||||
if (videos.isNullOrEmpty()) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
} else {
|
|
||||||
val attachments = videos.map {
|
|
||||||
it.toContentAttachmentData()
|
|
||||||
}
|
|
||||||
callback.onContentAttachmentsReady(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(error: String?) {
|
|
||||||
callback.onAttachmentsProcessFailed()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +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 android.app.Activity
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.kbeanie.multipicker.api.AudioPicker
|
|
||||||
import com.kbeanie.multipicker.api.CameraImagePicker
|
|
||||||
import com.kbeanie.multipicker.api.ContactPicker
|
|
||||||
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
|
|
||||||
|
|
||||||
fun createCameraImagePicker(): CameraImagePicker
|
|
||||||
|
|
||||||
fun createVideoPicker(): VideoPicker
|
|
||||||
|
|
||||||
fun createFilePicker(): FilePicker
|
|
||||||
|
|
||||||
fun createAudioPicker(): AudioPicker
|
|
||||||
|
|
||||||
fun createContactPicker(): ContactPicker
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
|
||||||
|
|
||||||
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
|
||||||
|
|
||||||
override fun createImagePicker(): ImagePicker {
|
|
||||||
return ImagePicker(activity).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCameraImagePicker(): CameraImagePicker {
|
|
||||||
return CameraImagePicker(activity).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createVideoPicker(): VideoPicker {
|
|
||||||
return VideoPicker(activity).also {
|
|
||||||
it.setVideoPickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFilePicker(): FilePicker {
|
|
||||||
return FilePicker(activity).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setFilePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createAudioPicker(): AudioPicker {
|
|
||||||
return AudioPicker(activity).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setAudioPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createContactPicker(): ContactPicker {
|
|
||||||
return ContactPicker(activity).also {
|
|
||||||
it.setContactPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
|
||||||
|
|
||||||
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
|
||||||
|
|
||||||
override fun createImagePicker(): ImagePicker {
|
|
||||||
return ImagePicker(fragment).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCameraImagePicker(): CameraImagePicker {
|
|
||||||
return CameraImagePicker(fragment).also {
|
|
||||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createVideoPicker(): VideoPicker {
|
|
||||||
return VideoPicker(fragment).also {
|
|
||||||
it.setVideoPickerCallback(attachmentsPickerCallback)
|
|
||||||
it.allowMultiple()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFilePicker(): FilePicker {
|
|
||||||
return FilePicker(fragment).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setFilePickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createAudioPicker(): AudioPicker {
|
|
||||||
return AudioPicker(fragment).also {
|
|
||||||
it.allowMultiple()
|
|
||||||
it.setAudioPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createContactPicker(): ContactPicker {
|
|
||||||
return ContactPicker(fragment).also {
|
|
||||||
it.setContactPickerCallback(attachmentsPickerCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyControlle
|
||||||
override fun buildModels(data: AttachmentsPreviewViewState) {
|
override fun buildModels(data: AttachmentsPreviewViewState) {
|
||||||
data.attachments.forEach {
|
data.attachments.forEach {
|
||||||
attachmentBigPreviewItem {
|
attachmentBigPreviewItem {
|
||||||
id(it.path)
|
id(it.queryUri.toString())
|
||||||
attachment(it)
|
attachment(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyCon
|
||||||
override fun buildModels(data: AttachmentsPreviewViewState) {
|
override fun buildModels(data: AttachmentsPreviewViewState) {
|
||||||
data.attachments.forEachIndexed { index, contentAttachmentData ->
|
data.attachments.forEachIndexed { index, contentAttachmentData ->
|
||||||
attachmentMiniaturePreviewItem {
|
attachmentMiniaturePreviewItem {
|
||||||
id(contentAttachmentData.path)
|
id(contentAttachmentData.queryUri.toString())
|
||||||
attachment(contentAttachmentData)
|
attachment(contentAttachmentData)
|
||||||
checked(data.currentAttachmentIndex == index)
|
checked(data.currentAttachmentIndex == index)
|
||||||
clickListener { _ ->
|
clickListener { _ ->
|
||||||
|
|
|
@ -33,11 +33,10 @@ abstract class AttachmentPreviewItem<H : AttachmentPreviewItem.Holder> : VectorE
|
||||||
abstract val attachment: ContentAttachmentData
|
abstract val attachment: ContentAttachmentData
|
||||||
|
|
||||||
override fun bind(holder: H) {
|
override fun bind(holder: H) {
|
||||||
val path = attachment.path
|
|
||||||
if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) {
|
if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) {
|
||||||
Glide.with(holder.view.context)
|
Glide.with(holder.view.context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(path)
|
.load(attachment.queryUri)
|
||||||
.apply(RequestOptions().frame(0))
|
.apply(RequestOptions().frame(0))
|
||||||
.into(holder.imageView)
|
.into(holder.imageView)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.attachments.preview
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class AttachmentsPreviewAction : VectorViewModelAction {
|
sealed class AttachmentsPreviewAction : VectorViewModelAction {
|
||||||
object RemoveCurrentAttachment : AttachmentsPreviewAction()
|
object RemoveCurrentAttachment : AttachmentsPreviewAction()
|
||||||
data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction()
|
data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction()
|
||||||
data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction()
|
data class UpdatePathOfCurrentAttachment(val newUri: Uri): AttachmentsPreviewAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,9 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCropResult(result: Intent) {
|
private fun handleCropResult(result: Intent) {
|
||||||
val resultPath = UCrop.getOutput(result)?.path
|
val resultUri = UCrop.getOutput(result)
|
||||||
if (resultPath != null) {
|
if (resultUri != null) {
|
||||||
viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath))
|
viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri))
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
@ -202,8 +202,7 @@ class AttachmentsPreviewFragment @Inject constructor(
|
||||||
private fun doHandleEditAction() = withState(viewModel) {
|
private fun doHandleEditAction() = withState(viewModel) {
|
||||||
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
|
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
|
||||||
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
|
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
|
||||||
// Note: using currentAttachment.queryUri.toUri() make the app crash when sharing from Google Photos
|
val uri = currentAttachment.queryUri
|
||||||
val uri = File(currentAttachment.path).toUri()
|
|
||||||
UCrop.of(uri, destinationFile.toUri())
|
UCrop.of(uri, destinationFile.toUri())
|
||||||
.withOptions(
|
.withOptions(
|
||||||
UCrop.Options()
|
UCrop.Options()
|
||||||
|
|
|
@ -62,7 +62,7 @@ class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialS
|
||||||
private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState {
|
private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState {
|
||||||
val attachments = it.attachments.mapIndexed { index, contentAttachmentData ->
|
val attachments = it.attachments.mapIndexed { index, contentAttachmentData ->
|
||||||
if (index == it.currentAttachmentIndex) {
|
if (index == it.currentAttachmentIndex) {
|
||||||
contentAttachmentData.copy(path = action.newPath)
|
contentAttachmentData.copy(queryUri = action.newUri)
|
||||||
} else {
|
} else {
|
||||||
contentAttachmentData
|
contentAttachmentData
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,7 +250,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||||
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
|
||||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||||
setupToolbar(roomToolbar)
|
setupToolbar(roomToolbar)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
@ -290,9 +290,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
roomDetailViewModel.observeViewEvents {
|
roomDetailViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
||||||
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
||||||
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
||||||
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
||||||
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
|
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
|
||||||
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
|
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
|
||||||
|
@ -665,7 +665,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private fun sendUri(uri: Uri): Boolean {
|
private fun sendUri(uri: Uri): Boolean {
|
||||||
roomDetailViewModel.preventAttachmentPreview = true
|
roomDetailViewModel.preventAttachmentPreview = true
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||||
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent)
|
||||||
if (!isHandled) {
|
if (!isHandled) {
|
||||||
roomDetailViewModel.preventAttachmentPreview = false
|
roomDetailViewModel.preventAttachmentPreview = false
|
||||||
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||||
|
@ -1350,11 +1350,11 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||||
when (type) {
|
when (type) {
|
||||||
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
|
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(this)
|
||||||
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
|
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(this)
|
||||||
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
|
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(this)
|
||||||
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
|
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(this)
|
||||||
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
|
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(this)
|
||||||
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
|
@ -610,7 +610,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||||
null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
||||||
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
|
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
|
||||||
tooBigFile.name ?: tooBigFile.path,
|
tooBigFile.name ?: tooBigFile.queryUri.toString(),
|
||||||
tooBigFile.size,
|
tooBigFile.size,
|
||||||
maxUploadFileSize
|
maxUploadFileSize
|
||||||
))
|
))
|
||||||
|
|
|
@ -72,18 +72,18 @@ class IncomingShareFragment @Inject constructor(
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupToolbar(incomingShareToolbar)
|
setupToolbar(incomingShareToolbar)
|
||||||
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
|
||||||
|
|
||||||
val intent = vectorBaseActivity.intent
|
val intent = vectorBaseActivity.intent
|
||||||
val isShareManaged = when (intent?.action) {
|
val isShareManaged = when (intent?.action) {
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
var isShareManaged = attachmentsHelper.handleShareIntent(intent)
|
var isShareManaged = attachmentsHelper.handleShareIntent(requireContext(), intent)
|
||||||
if (!isShareManaged) {
|
if (!isShareManaged) {
|
||||||
isShareManaged = handleTextShare(intent)
|
isShareManaged = handleTextShare(intent)
|
||||||
}
|
}
|
||||||
isShareManaged
|
isShareManaged
|
||||||
}
|
}
|
||||||
Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(intent)
|
Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(requireContext(), intent)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue