diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5f614763d5..ca6c2ce6d9 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -119,7 +119,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.1.0' + implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' implementation 'id.zelory:compressor:3.0.0' // Database diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index e32bb9f21f..b80a17b017 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.content +import android.net.Uri import android.os.Parcelable import androidx.exifinterface.media.ExifInterface import kotlinx.android.parcel.Parcelize @@ -29,8 +30,7 @@ data class ContentAttachmentData( val width: Long? = 0, val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, val name: String? = null, - val queryUri: String, - val path: String, + val queryUri: Uri, private val mimeType: String?, val type: Type ) : Parcelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 4071c9224f..4fa0cb5013 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -53,9 +53,9 @@ internal class FileUploader @Inject constructor(@Authenticated suspend fun uploadByteArray(byteArray: ByteArray, filename: String?, - mimeType: String, + mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val uploadBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull()) + val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull()) return upload(uploadBody, filename, progressListener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt index 083cac0278..eae2bf8f6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt @@ -16,12 +16,12 @@ package im.vector.matrix.android.internal.session.content +import android.content.Context import android.graphics.Bitmap -import android.media.ThumbnailUtils -import android.provider.MediaStore +import android.media.MediaMetadataRetriever import im.vector.matrix.android.api.session.content.ContentAttachmentData +import timber.log.Timber import java.io.ByteArrayOutputStream -import java.io.File internal object ThumbnailExtractor { @@ -33,34 +33,40 @@ internal object ThumbnailExtractor { val mimeType: String ) - fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - val file = File(attachment.path) - if (!file.exists() || !file.isFile) { - return null - } + fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { return if (attachment.type == ContentAttachmentData.Type.VIDEO) { - extractVideoThumbnail(attachment) + extractVideoThumbnail(context, attachment) } else { null } } - private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) ?: return null - val outputStream = ByteArrayOutputStream() - thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) - val thumbnailWidth = thumbnail.width - val thumbnailHeight = thumbnail.height - val thumbnailSize = outputStream.size() - val thumbnailData = ThumbnailData( - width = thumbnailWidth, - height = thumbnailHeight, - size = thumbnailSize.toLong(), - bytes = outputStream.toByteArray(), - mimeType = "image/jpeg" - ) - thumbnail.recycle() - outputStream.reset() + private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + var thumbnailData: ThumbnailData? = null + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + mediaMetadataRetriever.setDataSource(context, attachment.queryUri) + val thumbnail = mediaMetadataRetriever.frameAtTime + + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + 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 } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 1c88f87804..1b736d349f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -17,12 +17,9 @@ package im.vector.matrix.android.internal.session.content import android.content.Context -import android.graphics.BitmapFactory import androidx.work.CoroutineWorker import androidx.work.WorkerParameters 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.events.model.Event 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 timber.log.Timber import java.io.ByteArrayInputStream -import java.io.File -import java.io.FileInputStream import javax.inject.Inject private data class NewImageAttributes( @@ -94,8 +89,90 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var newImageAttributes: NewImageAttributes? = null - val attachmentFile = try { - File(attachment.path) + try { + 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) { Timber.e(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) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index f10c40ded5..a4a6eb6972 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.send +import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface @@ -74,6 +75,7 @@ import javax.inject.Inject * The transactionId is used as loc */ internal class LocalEchoEventFactory @Inject constructor( + private val context: Context, @UserId private val userId: String, private val stringProvider: StringProvider, private val textPillsUtils: TextPillsUtils, @@ -266,14 +268,14 @@ internal class LocalEchoEventFactory @Inject constructor( height = height?.toInt() ?: 0, size = attachment.size.toInt() ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { 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 val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime @@ -281,7 +283,7 @@ internal class LocalEchoEventFactory @Inject constructor( val width = firstFrame?.width ?: 0 mediaDataRetriever.release() - val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let { + val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let { ThumbnailInfo( width = it.width, height = it.height, @@ -299,10 +301,10 @@ internal class LocalEchoEventFactory @Inject constructor( size = attachment.size, duration = attachment.duration?.toInt() ?: 0, // Glide will be able to use the local path and extract a thumbnail. - thumbnailUrl = attachment.path, + thumbnailUrl = attachment.queryUri.toString(), thumbnailInfo = thumbnailInfo ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } @@ -315,7 +317,7 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg", size = attachment.size ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } @@ -329,7 +331,7 @@ internal class LocalEchoEventFactory @Inject constructor( ?: "application/octet-stream", size = attachment.size ), - url = attachment.path + url = attachment.queryUri.toString() ) return createEvent(roomId, content) } diff --git a/multipicker/.gitignore b/multipicker/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/multipicker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/multipicker/build.gradle b/multipicker/build.gradle new file mode 100644 index 0000000000..8b08a9d3ef --- /dev/null +++ b/multipicker/build.gradle @@ -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' +} diff --git a/multipicker/consumer-rules.pro b/multipicker/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/multipicker/proguard-rules.pro b/multipicker/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/multipicker/proguard-rules.pro @@ -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 diff --git a/multipicker/src/main/AndroidManifest.xml b/multipicker/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e1f12697e0 --- /dev/null +++ b/multipicker/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt new file mode 100644 index 0000000000..05e4c337b6 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/AudioPicker.kt @@ -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(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 { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val audioList = mutableListOf() + + 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/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt new file mode 100644 index 0000000000..240d809373 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/CameraPicker.kt @@ -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 */ + ) + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt new file mode 100644 index 0000000000..b0ae0e4cda --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/ContactPicker.kt @@ -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(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 { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val contactList = mutableListOf() + + 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() + var emailList = mutableListOf() + + 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 + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt new file mode 100644 index 0000000000..e8c74fad19 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/FilePicker.kt @@ -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(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 { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val fileList = mutableListOf() + + 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 = "*/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt new file mode 100644 index 0000000000..d7bf383f03 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/ImagePicker.kt @@ -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(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 { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val imageList = mutableListOf() + + 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/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt new file mode 100644 index 0000000000..24769e11c3 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/MultiPicker.kt @@ -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 { + + companion object Type { + val IMAGE by lazy { MultiPicker() } + val FILE by lazy { MultiPicker() } + val VIDEO by lazy { MultiPicker() } + val AUDIO by lazy { MultiPicker() } + val CONTACT by lazy { MultiPicker() } + val CAMERA by lazy { MultiPicker() } + + 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 get(type: MultiPicker): 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") + } + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt new file mode 100644 index 0000000000..43ac5d5fdd --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/Picker.kt @@ -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(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 + + /** + * 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 { + if (data == null) return emptyList() + + val uriList = mutableListOf() + 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? = data.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + extraUriList?.let { uriList.addAll(it) } + } + + val resInfoList: List = 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 { + 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 { + val selectedUriList = mutableListOf() + 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()?.let { uriList -> + selectedUriList.addAll(uriList) + } + if (it is Uri) { + selectedUriList.add(it) + } + } + } + return selectedUriList + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt new file mode 100644 index 0000000000..b85ffacd48 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/VideoPicker.kt @@ -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(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 { + if (requestCode != this.requestCode && resultCode != Activity.RESULT_OK) { + return emptyList() + } + + val videoList = mutableListOf() + + 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/*" + } + } +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt new file mode 100644 index 0000000000..6afe022024 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerAudioType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt new file mode 100644 index 0000000000..777e4d8441 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerBaseType.kt @@ -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 +} diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt new file mode 100644 index 0000000000..a9135443a2 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerContactType.kt @@ -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, + val emailList: List +) diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt new file mode 100644 index 0000000000..5417520d28 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerFileType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt new file mode 100644 index 0000000000..b1aef171b4 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerImageType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt new file mode 100644 index 0000000000..ba9a8d233e --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/entity/MultiPickerVideoType.kt @@ -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 diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt new file mode 100644 index 0000000000..048b2ca199 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/provider/MultiPickerFileProvider.kt @@ -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() diff --git a/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt b/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt new file mode 100644 index 0000000000..c5171e7d84 --- /dev/null +++ b/multipicker/src/main/java/im/vector/riotx/multipicker/utils/ImageUtils.kt @@ -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 + } +} diff --git a/multipicker/src/main/res/xml/multipicker_provider_paths.xml b/multipicker/src/main/res/xml/multipicker_provider_paths.xml new file mode 100644 index 0000000000..ff9b81ce98 --- /dev/null +++ b/multipicker/src/main/res/xml/multipicker_provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d020abade4..04307e89d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch' +include ':multipicker' diff --git a/vector/build.gradle b/vector/build.gradle index 263c561921..eea99d418e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -254,6 +254,7 @@ dependencies { implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") implementation project(":diff-match-patch") + implementation project(":multipicker") implementation 'com.android.support:multidex:1.0.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -347,9 +348,6 @@ dependencies { // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.22@aar' - // File picker - implementation 'com.kbeanie:multipicker:1.6@aar' - // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 2e56e20ce7..092817a6cc 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index c576ebe1b9..daea538e12 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -18,20 +18,13 @@ package im.vector.riotx.features.attachments import android.app.Activity import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle 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.api.session.content.ContentAttachmentData 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 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. - * 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, - 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)) - } - } +class AttachmentsHelper(val context: Context, val callback: Callback) : Restorable { interface Callback { 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. - 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. 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 override fun onSaveInstanceState(outState: Bundle) { - capturePath?.also { - outState.putString(CAPTURE_PATH_KEY, it) + captureUri?.also { + outState.putParcelable(CAPTURE_PATH_KEY, it) } pendingType?.also { outState.putSerializable(PENDING_TYPE_KEY, it) @@ -106,10 +63,7 @@ class AttachmentsHelper private constructor(private val context: Context, } override fun onRestoreInstanceState(savedInstanceState: Bundle?) { - capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY) - if (capturePath != null) { - cameraImagePicker.reinitialize(capturePath) - } + captureUri = savedInstanceState?.getParcelable(CAPTURE_PATH_KEY) as? Uri 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 */ - fun selectFile() { - filePicker.pickFile() + fun selectFile(fragment: Fragment) { + MultiPicker.get(MultiPicker.FILE).startWith(fragment) } /** * Starts the process for handling image picking */ - fun selectGallery() { - imagePicker.pickImage() + fun selectGallery(fragment: Fragment) { + MultiPicker.get(MultiPicker.IMAGE).startWith(fragment) } /** * Starts the process for handling audio picking */ - fun selectAudio() { - audioPicker.pickAudio() + fun selectAudio(fragment: Fragment) { + MultiPicker.get(MultiPicker.AUDIO).startWith(fragment) } /** * Starts the process for handling capture image picking */ - fun openCamera() { - capturePath = cameraImagePicker.pickImage() + fun openCamera(fragment: Fragment) { + captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(fragment) } /** * Starts the process for handling contact picking */ - fun selectContact() { - contactPicker.pickContact() + fun selectContact(fragment: Fragment) { + 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 { if (resultCode == Activity.RESULT_OK) { - val pickerManager = getPickerManagerForRequestCode(requestCode) - if (pickerManager != null) { - if (pickerManager is ImagePickerImpl) { - pickerManager.reinitialize(capturePath) + when (requestCode) { + MultiPicker.REQUEST_CODE_PICK_FILE -> { + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.FILE) + .getSelectedFiles(context, requestCode, resultCode, data) + .map { it.toContentAttachmentData() } + ) } - pickerManager.submit(data) - return true + MultiPicker.REQUEST_CODE_PICK_AUDIO -> { + 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 } @@ -174,39 +172,35 @@ class AttachmentsHelper private constructor(private val context: Context, * * @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 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")) { - videoPicker.submit(safeShareIntent(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } 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("*")) { - filePicker.submit(safeShareIntent(intent)) + callback.onContentAttachmentsReady( + MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { + it.toContentAttachmentData() + } + ) } else { return false } 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 - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index a3de5084de..02b712b8a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -16,51 +16,48 @@ 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.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 -fun ChosenContact.toContactAttachment(): ContactAttachment { +fun MultiPickerContactType.toContactAttachment(): ContactAttachment { return ContactAttachment( displayName = displayName, photoUri = photoUri, - emails = emails.toList(), - phones = phones.toList() + emails = emailList.toList(), + phones = phoneNumberList.toList() ) } -fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerFileType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), size = size, - date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, - queryUri = queryUri + queryUri = contentUri ) } -fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), size = size, - date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, duration = duration, - queryUri = queryUri + queryUri = contentUri ) } -private fun ChosenFile.mapType(): ContentAttachmentData.Type { +private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type { return when { mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE 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") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = mapType(), name = displayName, @@ -80,23 +76,20 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { height = height.toLong(), width = width.toLong(), exifOrientation = orientation, - date = createdAt?.time ?: System.currentTimeMillis(), - queryUri = queryUri + queryUri = contentUri ) } -fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { +fun MultiPickerVideoType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( - path = originalPath, mimeType = mimeType, type = ContentAttachmentData.Type.VIDEO, size = size, - date = createdAt?.time ?: System.currentTimeMillis(), height = height.toLong(), width = width.toLong(), duration = duration, name = displayName, - queryUri = queryUri + queryUri = contentUri ) } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt deleted file mode 100644 index 62956e08c8..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt +++ /dev/null @@ -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?) { - if (audios.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = audios.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onFilesChosen(files: MutableList?) { - if (files.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = files.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onImagesChosen(images: MutableList?) { - if (images.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = images.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onVideosChosen(videos: MutableList?) { - if (videos.isNullOrEmpty()) { - callback.onAttachmentsProcessFailed() - } else { - val attachments = videos.map { - it.toContentAttachmentData() - } - callback.onContentAttachmentsReady(attachments) - } - } - - override fun onError(error: String?) { - callback.onAttachmentsProcessFailed() - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt deleted file mode 100644 index 6c03f21ab3..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt +++ /dev/null @@ -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) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt index 34f018aaf9..60ee722116 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewControllers.kt @@ -25,7 +25,7 @@ class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyControlle override fun buildModels(data: AttachmentsPreviewViewState) { data.attachments.forEach { attachmentBigPreviewItem { - id(it.path) + id(it.queryUri.toString()) attachment(it) } } @@ -43,7 +43,7 @@ class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyCon override fun buildModels(data: AttachmentsPreviewViewState) { data.attachments.forEachIndexed { index, contentAttachmentData -> attachmentMiniaturePreviewItem { - id(contentAttachmentData.path) + id(contentAttachmentData.queryUri.toString()) attachment(contentAttachmentData) checked(data.currentAttachmentIndex == index) clickListener { _ -> diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt index 3b43fa6e20..373298bf31 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentPreviewItems.kt @@ -33,11 +33,10 @@ abstract class AttachmentPreviewItem : VectorE abstract val attachment: ContentAttachmentData override fun bind(holder: H) { - val path = attachment.path if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) { Glide.with(holder.view.context) .asBitmap() - .load(path) + .load(attachment.queryUri) .apply(RequestOptions().frame(0)) .into(holder.imageView) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt index 5acc59b035..aef724331f 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewAction.kt @@ -17,10 +17,11 @@ package im.vector.riotx.features.attachments.preview +import android.net.Uri import im.vector.riotx.core.platform.VectorViewModelAction sealed class AttachmentsPreviewAction : VectorViewModelAction { object RemoveCurrentAttachment : AttachmentsPreviewAction() data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction() - data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction() + data class UpdatePathOfCurrentAttachment(val newUri: Uri): AttachmentsPreviewAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index e52b497df4..3b1972ffbc 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -172,9 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor( } private fun handleCropResult(result: Intent) { - val resultPath = UCrop.getOutput(result)?.path - if (resultPath != null) { - viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath)) + val resultUri = UCrop.getOutput(result) + if (resultUri != null) { + viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri)) } else { Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() } @@ -202,8 +202,7 @@ class AttachmentsPreviewFragment @Inject constructor( private fun doHandleEditAction() = withState(viewModel) { val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState 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 = File(currentAttachment.path).toUri() + val uri = currentAttachment.queryUri UCrop.of(uri, destinationFile.toUri()) .withOptions( UCrop.Options() diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt index 1f6c8c2f8b..d1e44fa963 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -62,7 +62,7 @@ class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialS private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState { val attachments = it.attachments.mapIndexed { index, contentAttachmentData -> if (index == it.currentAttachmentIndex) { - contentAttachmentData.copy(path = action.newPath) + contentAttachmentData.copy(queryUri = action.newUri) } else { contentAttachmentData } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e748478e6a..f58d7be718 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -250,7 +250,7 @@ class RoomDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - attachmentsHelper = AttachmentsHelper.create(this, this).register() + attachmentsHelper = AttachmentsHelper(requireContext(), this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(roomToolbar) setupRecyclerView() @@ -290,9 +290,9 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.observeViewEvents { when (it) { - is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) - is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) - is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) + is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) @@ -665,7 +665,7 @@ class RoomDetailFragment @Inject constructor( private fun sendUri(uri: Uri): Boolean { roomDetailViewModel.preventAttachmentPreview = true val shareIntent = Intent(Intent.ACTION_SEND, uri) - val isHandled = attachmentsHelper.handleShareIntent(shareIntent) + val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent) if (!isHandled) { roomDetailViewModel.preventAttachmentPreview = false 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) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera() - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() - AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() - AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact() + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(this) + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(this) + AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(this) + AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(this) + AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(this) AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") }.exhaustive } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 2ad90f073a..cef172da73 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -610,7 +610,7 @@ class RoomDetailViewModel @AssistedInject constructor( when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet()) else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError( - tooBigFile.name ?: tooBigFile.path, + tooBigFile.name ?: tooBigFile.queryUri.toString(), tooBigFile.size, maxUploadFileSize )) diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt index 74821ab2fe..aa665b5653 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareFragment.kt @@ -72,18 +72,18 @@ class IncomingShareFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupRecyclerView() setupToolbar(incomingShareToolbar) - attachmentsHelper = AttachmentsHelper.create(this, this).register() + attachmentsHelper = AttachmentsHelper(requireContext(), this).register() val intent = vectorBaseActivity.intent val isShareManaged = when (intent?.action) { Intent.ACTION_SEND -> { - var isShareManaged = attachmentsHelper.handleShareIntent(intent) + var isShareManaged = attachmentsHelper.handleShareIntent(requireContext(), intent) if (!isShareManaged) { isShareManaged = handleTextShare(intent) } isShareManaged } - Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(intent) + Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(requireContext(), intent) else -> false }