Added export file to Android's MediaStore from FileViewer

This commit is contained in:
Sylvain Berfini 2023-11-21 17:16:16 +01:00
parent 746ddf6457
commit 294f7f6fae
7 changed files with 242 additions and 8 deletions

View file

@ -21,6 +21,8 @@ package org.linphone.compatibility
import android.app.Notification
import android.app.Service
import android.net.Uri
import android.provider.MediaStore
import org.linphone.core.tools.Log
class Api28Compatibility {
@ -37,5 +39,20 @@ class Api28Compatibility {
Log.e("$TAG Can't start service as foreground! $e")
}
}
fun getMediaCollectionUri(isImage: Boolean, isVideo: Boolean, isAudio: Boolean): Uri {
return when {
isImage -> {
MediaStore.Images.Media.getContentUri("external")
}
isVideo -> {
MediaStore.Video.Media.getContentUri("external")
}
isAudio -> {
MediaStore.Audio.Media.getContentUri("external")
}
else -> Uri.EMPTY
}
}
}
}

View file

@ -21,7 +21,9 @@ package org.linphone.compatibility
import android.app.Notification
import android.app.Service
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import org.linphone.core.tools.Log
@ -46,5 +48,26 @@ class Api29Compatibility {
Log.e("$TAG Can't start service as foreground! $e")
}
}
fun getMediaCollectionUri(isImage: Boolean, isVideo: Boolean, isAudio: Boolean): Uri {
return when {
isImage -> {
MediaStore.Images.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY
)
}
isVideo -> {
MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY
)
}
isAudio -> {
MediaStore.Audio.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY
)
}
else -> Uri.EMPTY
}
}
}
}

View file

@ -22,6 +22,7 @@ package org.linphone.compatibility
import android.annotation.SuppressLint
import android.app.Notification
import android.app.Service
import android.net.Uri
import android.view.View
import org.linphone.mediastream.Version
@ -63,5 +64,17 @@ class Compatibility {
Api31Compatibility.removeBlurRenderEffect(view)
}
}
fun getMediaCollectionUri(
isImage: Boolean = false,
isVideo: Boolean = false,
isAudio: Boolean = false
): Uri {
return if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
Api29Compatibility.getMediaCollectionUri(isImage, isVideo, isAudio)
} else {
Api28Compatibility.getMediaCollectionUri(isImage, isVideo, isAudio)
}
}
}
}

View file

@ -1,8 +1,13 @@
package org.linphone.ui.main.viewer.viewmodel
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.net.Uri
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.ImageView
import androidx.annotation.UiThread
@ -13,7 +18,11 @@ import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
@ -45,6 +54,8 @@ class FileViewModel @UiThread constructor() : ViewModel() {
// Below are required for PDF viewer
private lateinit var pdfRenderer: PdfRenderer
private lateinit var filePath: String
var screenWidth: Int = 0
var screenHeight: Int = 0
// End of PDF viewer required variables
@ -58,6 +69,7 @@ class FileViewModel @UiThread constructor() : ViewModel() {
@UiThread
fun loadFile(file: String) {
filePath = file
val name = FileUtils.getNameFromFilePath(file)
fileName.value = name
@ -138,4 +150,143 @@ class FileViewModel @UiThread constructor() : ViewModel() {
isVideoPlaying.value = playVideo
toggleVideoPlayPauseEvent.value = Event(playVideo)
}
@UiThread
fun exportToMediaStore() {
if (::filePath.isInitialized) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
Log.i("$TAG Export file [$filePath] to Android's MediaStore")
if (addContentToMediaStore(filePath)) {
Log.i("$TAG File [$filePath] has been successfully exported to MediaStore")
// TODO: show toast
} else {
Log.e("$TAG Failed to export file [$filePath] to MediaStore!")
// TODO: show toast
}
}
}
} else {
Log.e("$TAG Filepath wasn't initialized!")
}
}
@UiThread
private suspend fun addContentToMediaStore(
path: String
): Boolean {
if (path.isEmpty()) {
Log.e("$TAG No file path to export to MediaStore!")
return false
}
val isImage = FileUtils.isExtensionImage(path)
val isVideo = FileUtils.isExtensionVideo(path)
val isAudio = FileUtils.isExtensionAudio(path)
val directory = when {
isImage -> Environment.DIRECTORY_PICTURES
isVideo -> Environment.DIRECTORY_MOVIES
isAudio -> Environment.DIRECTORY_MUSIC
else -> Environment.DIRECTORY_DOWNLOADS
}
val appName = AppUtils.getString(R.string.app_name)
val relativePath = "$directory/$appName"
val fileName = FileUtils.getNameFromFilePath(path)
val extension = FileUtils.getExtensionFromFileName(fileName)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val context = coreContext.context
val mediaStoreFilePath = when {
isImage -> {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, mime)
put(MediaStore.Images.Media.RELATIVE_PATH, relativePath)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val collection = Compatibility.getMediaCollectionUri(isImage = true)
addContentValuesToCollection(
context,
path,
collection,
values,
MediaStore.Images.Media.IS_PENDING
)
}
isVideo -> {
val values = ContentValues().apply {
put(MediaStore.Video.Media.TITLE, fileName)
put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
put(MediaStore.Video.Media.MIME_TYPE, mime)
put(MediaStore.Video.Media.RELATIVE_PATH, relativePath)
put(MediaStore.Video.Media.IS_PENDING, 1)
}
val collection = Compatibility.getMediaCollectionUri(isVideo = true)
addContentValuesToCollection(
context,
path,
collection,
values,
MediaStore.Video.Media.IS_PENDING
)
}
isAudio -> {
val values = ContentValues().apply {
put(MediaStore.Audio.Media.TITLE, fileName)
put(MediaStore.Audio.Media.DISPLAY_NAME, fileName)
put(MediaStore.Audio.Media.MIME_TYPE, mime)
put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath)
put(MediaStore.Audio.Media.IS_PENDING, 1)
}
val collection = Compatibility.getMediaCollectionUri(isAudio = true)
addContentValuesToCollection(
context,
path,
collection,
values,
MediaStore.Audio.Media.IS_PENDING
)
}
else -> ""
}
if (mediaStoreFilePath.isNotEmpty()) {
Log.i("$TAG Exported file path to MediaStore is: $mediaStoreFilePath")
return true
}
return false
}
@UiThread
private suspend fun addContentValuesToCollection(
context: Context,
filePath: String,
collection: Uri,
values: ContentValues,
pendingKey: String
): String {
try {
val fileUri = context.contentResolver.insert(collection, values)
if (fileUri == null) {
Log.e("$TAG Failed to get a URI to where store the file, aborting")
return ""
}
context.contentResolver.openOutputStream(fileUri).use { out ->
if (FileUtils.copyFileTo(filePath, out)) {
values.clear()
values.put(pendingKey, 0)
context.contentResolver.update(fileUri, values, null, null)
return fileUri.toString()
}
}
} catch (e: Exception) {
Log.e("$TAG Exception: $e")
}
return ""
}
}

View file

@ -35,6 +35,7 @@ import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -74,6 +75,13 @@ class FileUtils {
return getMimeType(type) == MimeType.Video
}
@AnyThread
fun isExtensionAudio(path: String): Boolean {
val extension = getExtensionFromFileName(path)
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return getMimeType(type) == MimeType.Audio
}
@AnyThread
fun getExtensionFromFileName(fileName: String): String {
var extension = MimeTypeMap.getFileExtensionFromUrl(fileName)
@ -251,6 +259,34 @@ class FileUtils {
return false
}
suspend fun copyFileTo(filePath: String, outputStream: OutputStream?): Boolean {
if (outputStream == null) {
Log.e("$TAG Can't copy file $filePath to given null output stream")
return false
}
val file = File(filePath)
if (!file.exists()) {
Log.e("$TAG Can't copy file $filePath, it doesn't exists")
return false
}
try {
withContext(Dispatchers.IO) {
val inputStream = FileInputStream(file)
val buffer = ByteArray(4096)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } >= 0) {
outputStream.write(buffer, 0, bytesRead)
}
}
return true
} catch (e: IOException) {
Log.e("$TAG copyFileTo exception: $e")
}
return false
}
suspend fun deleteFile(filePath: String) {
withContext(Dispatchers.IO) {
val file = File(filePath)

View file

@ -11,9 +11,6 @@
<variable
name="shareClickListener"
type="View.OnClickListener" />
<variable
name="saveClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.viewer.viewmodel.FileViewModel" />
@ -130,7 +127,7 @@
<ImageView
android:id="@+id/save"
android:onClick="@{saveClickListener}"
android:onClick="@{() -> viewModel.exportToMediaStore()}"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:background="@color/white"

View file

@ -11,9 +11,6 @@
<variable
name="shareClickListener"
type="View.OnClickListener" />
<variable
name="saveClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.viewer.viewmodel.FileViewModel" />
@ -130,7 +127,7 @@
<ImageView
android:id="@+id/save"
android:onClick="@{saveClickListener}"
android:onClick="@{() -> viewModel.exportToMediaStore()}"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:background="@color/white"