Merge pull request #516 from Brice-W/media-saving

Fix Saving Media Files
pull/541/head
Niels Andriesse 3 years ago committed by GitHub
commit e598d73539
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,26 +1,30 @@
package org.thoughtcrime.securesms.util
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.DialogInterface.OnClickListener
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.text.TextUtils
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import network.loki.messenger.R
import org.session.libsession.utilities.task.ProgressDialogAsyncTask
import org.session.libsignal.utilities.externalstorage.ExternalStorageUtil
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.mms.PartAuthority
import org.session.libsession.utilities.task.ProgressDialogAsyncTask
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import kotlin.jvm.Throws
import org.session.libsession.utilities.Util
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Saves attachment files to an external storage using [MediaStore] API.
@ -95,71 +99,120 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
@Throws(IOException::class)
private fun saveAttachment(context: Context, attachment: Attachment): String? {
val resolver = context.contentResolver
val contentType = MediaUtil.getCorrectedMimeType(attachment.contentType)!!
val fileName = attachment.fileName
?: sanitizeOutputFileName(generateOutputFileName(contentType, attachment.date))
val mediaRecord = ContentValues()
val mediaVolume = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
MediaStore.VOLUME_EXTERNAL
} else {
MediaStore.VOLUME_EXTERNAL_PRIMARY
val contentType = Objects.requireNonNull(MediaUtil.getCorrectedMimeType(attachment.contentType))!!
var fileName = attachment.fileName
if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date)
fileName = sanitizeOutputFileName(fileName)
val outputUri: Uri = getMediaStoreContentUriForType(contentType)
val mediaUri = createOutputUri(outputUri, contentType, fileName)
val updateValues = ContentValues()
PartAuthority.getAttachmentStream(context, attachment.uri).use { inputStream ->
if (inputStream == null) {
return null
}
if (outputUri.scheme == ContentResolver.SCHEME_FILE) {
FileOutputStream(mediaUri!!.path).use { outputStream ->
StreamUtil.copy(inputStream, outputStream)
MediaScannerConnection.scanFile(context, arrayOf(mediaUri.path), arrayOf(contentType), null)
}
} else {
context.contentResolver.openOutputStream(mediaUri!!, "w").use { outputStream ->
val total: Long = StreamUtil.copy(inputStream, outputStream)
if (total > 0) {
updateValues.put(MediaStore.MediaColumns.SIZE, total)
}
}
}
}
val collectionUri: Uri
when {
contentType.startsWith("video/") -> {
collectionUri = MediaStore.Video.Media.getContentUri(mediaVolume)
mediaRecord.put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
mediaRecord.put(MediaStore.Video.Media.MIME_TYPE, contentType)
// Add the date meta data to ensure the image is added at the front of the gallery
mediaRecord.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis())
mediaRecord.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
if (Build.VERSION.SDK_INT > 28) {
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
}
if (updateValues.size() > 0) {
getContext().contentResolver.update(mediaUri!!, updateValues, null, null)
}
return outputUri.lastPathSegment
}
}
contentType.startsWith("audio/") -> {
collectionUri = MediaStore.Audio.Media.getContentUri(mediaVolume)
mediaRecord.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName)
mediaRecord.put(MediaStore.Audio.Media.MIME_TYPE, contentType)
mediaRecord.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
mediaRecord.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis())
private fun getMediaStoreContentUriForType(contentType: String): Uri {
return when {
contentType.startsWith("video/") ->
ExternalStorageUtil.getVideoUri()
contentType.startsWith("audio/") ->
ExternalStorageUtil.getAudioUri()
contentType.startsWith("image/") ->
ExternalStorageUtil.getImageUri()
else ->
ExternalStorageUtil.getDownloadUri()
}
}
@Throws(IOException::class)
private fun createOutputUri(outputUri: Uri, contentType: String, fileName: String): Uri? {
val fileParts: Array<String> = getFileNameParts(fileName)
val base = fileParts[0]
val extension = fileParts[1]
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
contentValues.put(MediaStore.MediaColumns.DATE_ADDED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))
contentValues.put(MediaStore.MediaColumns.DATE_MODIFIED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))
if (Build.VERSION.SDK_INT > 28) {
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1)
} else if (Objects.equals(outputUri.scheme, ContentResolver.SCHEME_FILE)) {
val outputDirectory = File(outputUri.path)
var outputFile = File(outputDirectory, "$base.$extension")
var i = 0
while (outputFile.exists()) {
outputFile = File(outputDirectory, base + "-" + ++i + "." + extension)
}
contentType.startsWith("image/") -> {
collectionUri = MediaStore.Images.Media.getContentUri(mediaVolume)
mediaRecord.put(MediaStore.Images.Media.TITLE, fileName)
mediaRecord.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
mediaRecord.put(MediaStore.Images.Media.MIME_TYPE, contentType)
mediaRecord.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
mediaRecord.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
if (outputFile.isHidden) {
throw IOException("Specified name would not be visible")
}
else -> {
mediaRecord.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName)
collectionUri = MediaStore.Files.getContentUri(mediaVolume)
return Uri.fromFile(outputFile)
} else {
var outputFileName = fileName
var dataPath = String.format("%s/%s", getExternalPathToFileForType(contentType), outputFileName)
var i = 0
while (pathTaken(outputUri, dataPath)) {
Log.d(TAG, "The content exists. Rename and check again.")
outputFileName = base + "-" + ++i + "." + extension
dataPath = String.format("%s/%s", getExternalPathToFileForType(contentType), outputFileName)
}
contentValues.put(MediaStore.MediaColumns.DATA, dataPath)
}
return context.contentResolver.insert(outputUri, contentValues)
}
var mediaFileUri: Uri?
try {
mediaFileUri = resolver.insert(collectionUri, mediaRecord)
} catch (exception: Exception) {
return null
}
if (mediaFileUri == null) return null
private fun getFileNameParts(fileName: String): Array<String> {
val tokens = fileName.split("\\.(?=[^\\.]+$)".toRegex()).toTypedArray()
return arrayOf(tokens[0], if (tokens.size > 1) tokens[1] else "")
}
val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri)
if (inputStream == null) return null
private fun getExternalPathToFileForType(contentType: String): String {
val storage: File = when {
contentType.startsWith("video/") ->
context.getExternalFilesDir(Environment.DIRECTORY_MOVIES)!!
contentType.startsWith("audio/") ->
context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)!!
contentType.startsWith("image/") ->
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
else ->
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)!!
}
return storage.absolutePath
}
inputStream.use {
resolver.openOutputStream(mediaFileUri).use {
Util.copy(inputStream, it)
@Throws(IOException::class)
private fun pathTaken(outputUri: Uri, dataPath: String): Boolean {
context.contentResolver.query(outputUri, arrayOf(MediaStore.MediaColumns.DATA),
MediaStore.MediaColumns.DATA + " = ?", arrayOf(dataPath),
null).use { cursor ->
if (cursor == null) {
throw IOException("Something is wrong with the filename to save")
}
return cursor.moveToFirst()
}
return mediaFileUri.toString()
}
private fun generateOutputFileName(contentType: String, timestamp: Long): String {
@ -177,8 +230,7 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
override fun onPostExecute(result: Pair<Int, String?>) {
super.onPostExecute(result)
val context = contextReference.get()
if (context == null) return
val context = contextReference.get() ?: return
when (result.first) {
RESULT_FAILURE -> {

@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.Nullable;
import org.session.libsignal.utilities.logging.Log;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Utility methods for input and output streams.
*/
public final class StreamUtil {
private static final String TAG = Log.tag(StreamUtil.class);
private StreamUtil() {}
public static void close(@Nullable Closeable closeable) {
if (closeable == null) return;
try {
closeable.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
public static long getStreamLength(InputStream in) throws IOException {
byte[] buffer = new byte[4096];
int totalSize = 0;
int read;
while ((read = in.read(buffer)) != -1) {
totalSize += read;
}
return totalSize;
}
public static void readFully(InputStream in, byte[] buffer) throws IOException {
readFully(in, buffer, buffer.length);
}
public static void readFully(InputStream in, byte[] buffer, int len) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, len - offset);
if (read == -1) throw new EOFException("Stream ended early");
if (read + offset < len) offset += read;
else return;
}
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
bout.write(buffer, 0, read);
}
in.close();
return bout.toByteArray();
}
public static String readFullyAsString(InputStream in) throws IOException {
return new String(readFully(in));
}
public static long copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[64 * 1024];
int read;
long total = 0;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
total += read;
}
in.close();
out.close();
return total;
}
}

@ -1,7 +1,10 @@
package org.session.libsignal.utilities.externalstorage
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import java.io.File
object ExternalStorageUtil {
@ -45,6 +48,30 @@ object ExternalStorageUtil {
return context.externalCacheDir
}
fun getVideoUri(): Uri {
return MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
fun getAudioUri(): Uri {
return MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
fun getImageUri(): Uri {
return MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
fun getDownloadUri(): Uri {
if (Build.VERSION.SDK_INT < 29) {
return getLegacyUri(Environment.DIRECTORY_DOWNLOADS);
} else {
return MediaStore.Downloads.EXTERNAL_CONTENT_URI;
}
}
private fun getLegacyUri(directory: String): Uri {
return Uri.fromFile(Environment.getExternalStoragePublicDirectory(directory))
}
@JvmStatic
fun getCleanFileName(fileName: String?): String? {
var fileName = fileName ?: return null

Loading…
Cancel
Save