diff --git a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt
index 7897de9ee..8175b495b 100644
--- a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt
@@ -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
+ }
+ }
}
}
diff --git a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt
index e0b654d84..de6159120 100644
--- a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt
@@ -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
+ }
+ }
}
}
diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt
index 9db7fb995..83c111a05 100644
--- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt
@@ -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)
+ }
+ }
}
}
diff --git a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt
index e5b12259b..b509e10a5 100644
--- a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt
@@ -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 ""
+ }
}
diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt
index 3c6f996cf..557b35ba8 100644
--- a/app/src/main/java/org/linphone/utils/FileUtils.kt
+++ b/app/src/main/java/org/linphone/utils/FileUtils.kt
@@ -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)
diff --git a/app/src/main/res/layout-land/file_viewer_fragment.xml b/app/src/main/res/layout-land/file_viewer_fragment.xml
index bbc39ccec..f7f97960a 100644
--- a/app/src/main/res/layout-land/file_viewer_fragment.xml
+++ b/app/src/main/res/layout-land/file_viewer_fragment.xml
@@ -11,9 +11,6 @@
-
@@ -130,7 +127,7 @@
-
@@ -130,7 +127,7 @@