add vector multipicker library

master
dfsek 1 year ago
parent 4d27e707bb
commit c74b79dbfc
  1. 1
      app/build.gradle.kts
  2. 5
      app/src/main/java/com/dfsek/dfchat/ui/RoomActivity.kt
  3. 5
      app/src/main/java/com/dfsek/dfchat/util/Utils.kt
  4. 2
      settings.gradle.kts
  5. 3
      vector/README.md
  6. 1
      vector/core-utils/.gitignore
  7. 1
      vector/core-utils/README.md
  8. 41
      vector/core-utils/build.gradle.kts
  9. 0
      vector/core-utils/consumer-rules.pro
  10. 21
      vector/core-utils/proguard-rules.pro
  11. 2
      vector/core-utils/src/main/AndroidManifest.xml
  12. 70
      vector/core-utils/src/main/java/im/vector/lib/core/utils/compat/Compat.kt
  13. 1
      vector/multipicker/.gitignore
  14. 3
      vector/multipicker/README.md
  15. 43
      vector/multipicker/build.gradle.kts
  16. 0
      vector/multipicker/consumer-rules.pro
  17. 21
      vector/multipicker/proguard-rules.pro
  18. 16
      vector/multipicker/src/main/AndroidManifest.xml
  19. 46
      vector/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt
  20. 69
      vector/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt
  21. 69
      vector/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt
  22. 139
      vector/multipicker/src/main/java/im/vector/lib/multipicker/ContactPicker.kt
  23. 83
      vector/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt
  24. 46
      vector/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt
  25. 57
      vector/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt
  26. 46
      vector/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt
  27. 108
      vector/multipicker/src/main/java/im/vector/lib/multipicker/Picker.kt
  28. 46
      vector/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt
  29. 28
      vector/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerAudioType.kt
  30. 23
      vector/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerBaseMediaType.kt
  31. 27
      vector/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerBaseType.kt
  32. 24
      vector/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerContactType.kt
  33. 26
      vector/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerFileType.kt
  34. 29
      vector/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerImageType.kt
  35. 30
      vector/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerVideoType.kt
  36. 21
      vector/multipicker/src/main/java/im/vector/lib/multipicker/provider/MultiPickerFileProvider.kt
  37. 162
      vector/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt
  38. 29
      vector/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt
  39. 60
      vector/multipicker/src/main/java/im/vector/lib/multipicker/utils/ImageUtils.kt
  40. 43
      vector/multipicker/src/main/java/im/vector/lib/multipicker/utils/MediaFileUtils.kt
  41. 21
      vector/multipicker/src/main/java/im/vector/lib/multipicker/utils/MimeTypeUtils.kt
  42. 6
      vector/multipicker/src/main/res/xml/multipicker_provider_paths.xml

@ -50,6 +50,7 @@ dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation(project(":vector:multipicker"))
val room_version = "2.4.3"
implementation("androidx.room:room-runtime:$room_version")

@ -52,6 +52,7 @@ import com.dfsek.dfchat.util.CameraFileProvider
import com.dfsek.dfchat.util.RenderMessage
import com.dfsek.dfchat.util.TimelineEventWrapper
import com.dfsek.dfchat.util.getPreviewText
import im.vector.lib.multipicker.ImagePicker
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.events.model.toModel
@ -64,7 +65,7 @@ import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import kotlin.math.roundToInt
import im.vector.lib.multipicker.
class RoomActivity : AppCompatActivity() {
private lateinit var chatRoomState: ChatRoomState
@ -425,6 +426,7 @@ class RoomActivity : AppCompatActivity() {
.collect {
if(it) {
imageUri?.let {
it.toMultiPickerImageType()
launch {
chatRoomState.uploadImage(this@RoomActivity, it)
}
@ -445,6 +447,7 @@ class RoomActivity : AppCompatActivity() {
Column(modifier = Modifier.padding(24.dp)) {
Text("Upload Image", fontSize = 18.sp, modifier = Modifier.padding(6.dp))
Button(onClick = {
MultiPicker.Type
imagePicker.launch("image/*")
}) {
Text("Choose from Gallery")

@ -3,6 +3,7 @@ package com.dfsek.dfchat.util
import android.content.ActivityNotFoundException
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsSession
@ -11,6 +12,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.LiveData
import com.dfsek.dfchat.AppState
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
fun openUrlInChromeCustomTab(
@ -64,4 +66,5 @@ fun Color.toHexString(): String {
fun String.toColor(): Color {
return Color(android.graphics.Color.parseColor(this))
}
}

@ -16,3 +16,5 @@ dependencyResolutionManagement {
rootProject.name = "dfChat"
include(":app")
include(":vector:multipicker")
include(":vector:core-utils")

@ -0,0 +1,3 @@
# Vector Packages
Some useful stuff from Vector that isn't in the Matrix SDK.

@ -0,0 +1 @@
/build

@ -0,0 +1 @@
Some core-utils classes from Vector

@ -0,0 +1,41 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "im.vector.lib.core.utils"
compileSdk = 33
defaultConfig {
minSdk = 24
targetSdk = 33
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}

@ -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

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

@ -0,0 +1,70 @@
/*
* Copyright (c) 2022 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.lib.core.utils.compat
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import java.io.Serializable
inline fun <reified T> Intent.getParcelableExtraCompat(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getParcelableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T?
}
inline fun <reified T : Parcelable> Intent.getParcelableArrayListExtraCompat(key: String): ArrayList<T>? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getParcelableArrayListExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getParcelableArrayListExtra<T>(key)
}
inline fun <reified T> Bundle.getParcelableCompat(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getParcelable(key, T::class.java)
else -> @Suppress("DEPRECATION") getParcelable(key) as? T?
}
inline fun <reified T : Serializable> Bundle.getSerializableCompat(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as? T?
}
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T?
}
fun PackageManager.queryIntentActivitiesCompat(data: Intent, flags: Int): List<ResolveInfo> {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> queryIntentActivities(
data,
PackageManager.ResolveInfoFlags.of(flags.toLong())
)
else -> @Suppress("DEPRECATION") queryIntentActivities(data, flags)
}
}
fun PackageManager.resolveActivityCompat(data: Intent, flags: Int): ResolveInfo? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> resolveActivity(
data,
PackageManager.ResolveInfoFlags.of(flags.toLong())
)
else -> @Suppress("DEPRECATION") resolveActivity(data, flags)
}
}

@ -0,0 +1,3 @@
# Multipicker
Classes from Vector (Element) MultiPicker library.

@ -0,0 +1,43 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "im.vector.lib.multipicker"
compileSdk = 32
defaultConfig {
minSdk = 24
targetSdk = 32
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("com.google.android.material:material:1.7.0")
implementation("androidx.exifinterface:exifinterface:1.3.5")
implementation(project(":vector:core-utils"))
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}

@ -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

@ -0,0 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.lib.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,46 @@
/*
* 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.lib.multipicker
import android.content.Context
import android.content.Intent
import im.vector.lib.multipicker.entity.MultiPickerAudioType
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
/**
* Audio file picker implementation.
*/
class AudioPicker : Picker<MultiPickerAudioType>() {
/**
* Call this function from onActivityResult(int, int, Intent).
* Returns selected audio files or empty list if user did not select any files.
*/
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerAudioType> {
return getSelectedUriList(data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerAudioType(context)
}
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "audio/*"
}
}
}

@ -0,0 +1,69 @@
/*
* 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.lib.multipicker
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.FileProvider
import im.vector.lib.multipicker.entity.MultiPickerImageType
import im.vector.lib.multipicker.utils.MediaType
import im.vector.lib.multipicker.utils.createTemporaryMediaFile
import im.vector.lib.multipicker.utils.toMultiPickerImageType
/**
* Implementation of taking a photo with Camera.
*/
class CameraPicker {
/**
* Start camera by using a ActivityResultLauncher.
* @return Uri of taken photo or null if the operation is cancelled.
*/
fun startWithExpectingFile(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>): Uri {
val photoUri = createPhotoUri(context)
val intent = createIntent().apply {
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
}
activityResultLauncher.launch(intent)
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, photoUri: Uri): MultiPickerImageType? {
return photoUri.toMultiPickerImageType(context)
}
private fun createIntent(): Intent {
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
}
companion object {
fun createPhotoUri(context: Context): Uri {
val file = createTemporaryMediaFile(context, MediaType.IMAGE)
val authority = context.packageName + ".multipicker.fileprovider"
return FileProvider.getUriForFile(context, authority, file)
}
}
}

@ -0,0 +1,69 @@
/*
* Copyright (c) 2021 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.lib.multipicker
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.FileProvider
import im.vector.lib.multipicker.entity.MultiPickerVideoType
import im.vector.lib.multipicker.utils.MediaType
import im.vector.lib.multipicker.utils.createTemporaryMediaFile
import im.vector.lib.multipicker.utils.toMultiPickerVideoType
/**
* Implementation of taking a video with Camera.
*/
class CameraVideoPicker {
/**
* Start camera by using a ActivityResultLauncher.
* @return Uri of taken photo or null if the operation is cancelled.
*/
fun startWithExpectingFile(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>): Uri {
val videoUri = createVideoUri(context)
val intent = createIntent().apply {
putExtra(MediaStore.EXTRA_OUTPUT, videoUri)
}
activityResultLauncher.launch(intent)
return videoUri
}
/**
* 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 getTakenVideo(context: Context, videoUri: Uri): MultiPickerVideoType? {
return videoUri.toMultiPickerVideoType(context)
}
private fun createIntent(): Intent {
return Intent(MediaStore.ACTION_VIDEO_CAPTURE)
}
companion object {
fun createVideoUri(context: Context): Uri {
val file = createTemporaryMediaFile(context, MediaType.VIDEO)
val authority = context.packageName + ".multipicker.fileprovider"
return FileProvider.getUriForFile(context, authority, file)
}
}
}

@ -0,0 +1,139 @@
/*
* 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.lib.multipicker
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.provider.ContactsContract
import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull
import im.vector.lib.multipicker.entity.MultiPickerContactType
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
/**
* Contact Picker implementation.
*/
class ContactPicker : Picker<MultiPickerContactType>() {
/**
* Call this function from onActivityResult(int, int, Intent).
* Returns selected contact or empty list if user did not select any contacts.
*/
@SuppressLint("Recycle")
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerContactType> {
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.getColumnIndexOrNull(ContactsContract.Contacts._ID) ?: return@use
val nameColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
val photoUriColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.PHOTO_URI) ?: return@use
val contactId = cursor.getIntOrNull(idColumn) ?: return@use
var name = cursor.getStringOrNull(nameColumn) ?: return@use
val photoUri = cursor.getStringOrNull(photoUriColumn)
val phoneNumberList = mutableListOf<String>()
val 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 inner@{ innerCursor ->
val mimeTypeColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.MIMETYPE) ?: return@inner
val data1ColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.DATA1) ?: return@inner
while (innerCursor.moveToNext()) {
val mimeType = innerCursor.getStringOrNull(mimeTypeColumnIndex)
val contactData = innerCursor.getStringOrNull(data1ColumnIndex) ?: continue
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.getColumnIndexOrNull(ContactsContract.RawContacts._ID)
?.let { cursor.getIntOrNull(it) }
} 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,83 @@
/*
* 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.lib.multipicker
import android.content.Context
import android.content.Intent
import android.provider.OpenableColumns
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
import im.vector.lib.multipicker.entity.MultiPickerBaseType
import im.vector.lib.multipicker.entity.MultiPickerFileType
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
import im.vector.lib.multipicker.utils.isMimeTypeAudio
import im.vector.lib.multipicker.utils.isMimeTypeImage
import im.vector.lib.multipicker.utils.isMimeTypeVideo
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import im.vector.lib.multipicker.utils.toMultiPickerImageType
import im.vector.lib.multipicker.utils.toMultiPickerVideoType
/**
* Implementation of selecting any type of files.
*/
class FilePicker : Picker<MultiPickerBaseType>() {
/**
* Call this function from onActivityResult(int, int, Intent).
* Returns selected files or empty list if user did not select any files.
*/
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseType> {
return getSelectedUriList(data).mapNotNull { selectedUri ->
val type = context.contentResolver.getType(selectedUri)
when {
type.isMimeTypeVideo() -> selectedUri.toMultiPickerVideoType(context)
type.isMimeTypeImage() -> selectedUri.toMultiPickerImageType(context)
type.isMimeTypeAudio() -> selectedUri.toMultiPickerAudioType(context)
else -> {
// Other files
context.contentResolver.query(selectedUri, null, null, null, null)
?.use { cursor ->
val nameColumn = cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME) ?: return@use null
val sizeColumn = cursor.getColumnIndexOrNull(OpenableColumns.SIZE) ?: return@use null
if (cursor.moveToFirst()) {
val name = cursor.getStringOrNull(nameColumn)
val size = cursor.getLongOrNull(sizeColumn) ?: 0
MultiPickerFileType(
name,
size,
context.contentResolver.getType(selectedUri),
selectedUri
)
} else {
null
}
}
}
}
}
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "*/*"
}
}
}

@ -0,0 +1,46 @@
/*
* 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.lib.multipicker
import android.content.Context
import android.content.Intent
import im.vector.lib.multipicker.entity.MultiPickerImageType
import im.vector.lib.multipicker.utils.toMultiPickerImageType
/**
* Image Picker implementation.
*/
class ImagePicker : Picker<MultiPickerImageType>() {
/**
* Call this function from onActivityResult(int, int, Intent).
* Returns selected image files or empty list if user did not select any files.
*/
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerImageType> {
return getSelectedUriList(data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerImageType(context)
}
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "image/*"
}
}
}

@ -0,0 +1,57 @@
/*
* 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.lib.multipicker
import android.content.Context
import android.content.Intent
import im.vector.lib.multipicker.entity.MultiPickerBaseMediaType
import im.vector.lib.multipicker.utils.isMimeTypeVideo
import im.vector.lib.multipicker.utils.toMultiPickerImageType
import im.vector.lib.multipicker.utils.toMultiPickerVideoType
/**
* Image/Video Picker implementation.
*/
class MediaPicker : Picker<MultiPickerBaseMediaType>() {
/**
* Call this function from onActivityResult(int, int, Intent).
* Returns selected image/video files or empty list if user did not select any files.
*/
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseMediaType> {
return getSelectedUriList(data).mapNotNull { selectedUri ->
val mimeType = context.contentResolver.getType(selectedUri)
if (mimeType.isMimeTypeVideo()) {
selectedUri.toMultiPickerVideoType(context)
} else {
// Assume it's an image
selectedUri.toMultiPickerImageType(context)
}
}
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "*/*"
val mimeTypes = arrayOf("image/*", "video/*")
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
}
}
}

@ -0,0 +1,46 @@
/*
* 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.lib.multipicker
class MultiPicker<T> private constructor() {
companion object Type {
val IMAGE by lazy { MultiPicker<ImagePicker>() }
val MEDIA by lazy { MultiPicker<MediaPicker>() }
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>() }
val CAMERA_VIDEO by lazy { MultiPicker<CameraVideoPicker>() }
@Suppress("UNCHECKED_CAST")
fun <T> get(type: MultiPicker<T>): T {
return when (type) {
IMAGE -> ImagePicker() as T
VIDEO -> VideoPicker() as T
MEDIA -> MediaPicker() as T
FILE -> FilePicker() as T
AUDIO -> AudioPicker() as T
CONTACT -> ContactPicker() as T
CAMERA -> CameraPicker() as T
CAMERA_VIDEO -> CameraVideoPicker() as T
else -> throw IllegalArgumentException("Unsupported type $type")
}
}
}
}

@ -0,0 +1,108 @@
/*
* 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.lib.multipicker
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import androidx.activity.result.ActivityResultLauncher
import im.vector.lib.core.utils.compat.getParcelableArrayListExtraCompat
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import im.vector.lib.core.utils.compat.queryIntentActivitiesCompat
/**
* Abstract class to provide all types of Pickers.
*/
abstract class Picker<T> {
protected var single = false
/**
* Call this function from onActivityResult(int, int, Intent).
* @return selected files or empty list if user did not select any files.
*/
abstract fun getSelectedFiles(context: Context, 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.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)?.let { uriList.add(it) }
} else if (data.action == Intent.ACTION_SEND_MULTIPLE) {
val extraUriList: List<Uri>? = data.getParcelableArrayListExtraCompat(Intent.EXTRA_STREAM)
extraUriList?.let { uriList.addAll(it) }
}
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivitiesCompat(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, 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 a ActivityResultLauncher.
* @param activityResultLauncher to handle the result.
*/
fun startWith(activityResultLauncher: ActivityResultLauncher<Intent>) {
activityResultLauncher.launch(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) })
}
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,46 @@
/*
* 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.lib.multipicker
import android.content.Context
import android.content.Intent
import im.vector.lib.multipicker.entity.MultiPickerVideoType
import im.vector.lib.multipicker.utils.toMultiPickerVideoType
/**
* Video Picker implementation.
*/
class VideoPicker : Picker<MultiPickerVideoType>() {
/**
* Call this function from onActivityResult(int, int, Intent).
* Returns selected video files or empty list if user did not select any files.
*/
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerVideoType> {
return getSelectedUriList(data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerVideoType(context)
}
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "video/*"
}
}
}

@ -0,0 +1,28 @@
/*
* 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.lib.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,
var waveform: List<Int>? = null
) : MultiPickerBaseType

@ -0,0 +1,23 @@
/*
* 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.lib.multipicker.entity
interface MultiPickerBaseMediaType : MultiPickerBaseType {
val width: Int
val height: Int
val orientation: Int
}

@ -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.lib.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.lib.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.lib.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.lib.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,
override val width: Int,
override val height: Int,
override val orientation: Int
) : MultiPickerBaseMediaType

@ -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.lib.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,
override val width: Int,
override val height: Int,
override val orientation: Int,
val duration: Long
) : MultiPickerBaseMediaType

@ -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.lib.multipicker.provider
import androidx.core.content.FileProvider
class MultiPickerFileProvider : FileProvider()

@ -0,0 +1,162 @@
/*
* Copyright (c) 2021 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.lib.multipicker.utils
import android.content.Context
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.provider.MediaStore
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
import im.vector.lib.multipicker.entity.MultiPickerAudioType
import im.vector.lib.multipicker.entity.MultiPickerImageType
import im.vector.lib.multipicker.entity.MultiPickerVideoType
internal fun Uri.toMultiPickerImageType(context: Context): MultiPickerImageType? {
val projection = arrayOf(
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.SIZE
)
return context.contentResolver.query(
this,
projection,
null,
null,
null
)?.use { cursor ->
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.DISPLAY_NAME) ?: return@use null
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.SIZE) ?: return@use null
if (cursor.moveToNext()) {
val name = cursor.getStringOrNull(nameColumn)
val size = cursor.getLongOrNull(sizeColumn) ?: 0
val bitmap = ImageUtils.getBitmap(context, this)
val orientation = ImageUtils.getOrientation(context, this)
MultiPickerImageType(
name,
size,
context.contentResolver.getType(this),
this,
bitmap?.width ?: 0,
bitmap?.height ?: 0,
orientation
)
} else {
null
}
}
}
internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType? {
val projection = arrayOf(
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.SIZE
)
return context.contentResolver.query(
this,
projection,
null,
null,
null
)?.use { cursor ->
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.DISPLAY_NAME) ?: return@use null
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.SIZE) ?: return@use null
if (cursor.moveToNext()) {
val name = cursor.getStringOrNull(nameColumn)
val size = cursor.getLongOrNull(sizeColumn) ?: 0
var duration = 0L
var width = 0
var height = 0
var orientation = 0
context.contentResolver.openFileDescriptor(this, "r")?.use { pfd ->
val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0
}
MultiPickerVideoType(
name,
size,
context.contentResolver.getType(this),
this,
width,
height,
orientation,
duration
)
} else {
null
}
}
}
fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
val projection = arrayOf(
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.SIZE
)
return context.contentResolver.query(
this,
projection,
null,
null,
null
)?.use { cursor ->
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.DISPLAY_NAME) ?: return@use null
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.SIZE) ?: return@use null
if (cursor.moveToNext()) {
val name = cursor.getStringOrNull(nameColumn)
val size = cursor.getLongOrNull(sizeColumn) ?: 0
var duration = 0L
context.contentResolver.openFileDescriptor(this, "r")?.use { pfd ->
val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
}
MultiPickerAudioType(
name,
size,
sanitize(context.contentResolver.getType(this)),
this,
duration
)
} else {
null
}
}
}
private fun sanitize(type: String?): String? {
if (type == "application/ogg") {
// Not supported on old system
return "audio/ogg"
}
return type
}

@ -0,0 +1,29 @@
/*
* Copyright (c) 2021 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.lib.multipicker.utils
import android.database.Cursor
import androidx.core.database.getStringOrNull
fun Cursor.getColumnIndexOrNull(column: String): Int? {
return getColumnIndex(column).takeIf { it != -1 }
}
fun Cursor.readStringColumnOrNull(column: String): String? {
return getColumnIndexOrNull(column)?.let { getStringOrNull(it) }
}

@ -0,0 +1,60 @@
/*
* 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.lib.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 android.util.Log
import androidx.exifinterface.media.ExifInterface
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) {
e.printStackTrace()
Log.e("Decoding Failed", "Cannot decode Bitmap: $uri")
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) {
e.printStackTrace()
Log.e("Decoding Failed", "Cannot read orientation: $uri")
}
}
return orientation
}
}

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 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.lib.multipicker.utils
import android.content.Context
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
internal fun createTemporaryMediaFile(context: Context, mediaType: MediaType): File {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir: File = context.filesDir.also { it.mkdirs() }
val fileSuffix = when (mediaType) {
MediaType.IMAGE -> ".jpg"
MediaType.VIDEO -> ".mp4"
}
return File.createTempFile(
"${timeStamp}_",
fileSuffix,
storageDir
)
}
internal enum class MediaType {
IMAGE, VIDEO
}

@ -0,0 +1,21 @@
/*
* Copyright (c) 2021 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.lib.multipicker.utils
internal fun String?.isMimeTypeImage() = this?.startsWith("image/") == true
internal fun String?.isMimeTypeVideo() = this?.startsWith("video/") == true
internal fun String?.isMimeTypeAudio() = this?.startsWith("audio/") == true

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="external_files"
path="." />
</paths>
Loading…
Cancel
Save