import of Signal's code to deal with attachments saving

This commit is contained in:
Brice-W 2021-04-28 09:22:19 +10:00
parent 9f26436041
commit a5e376b616
3 changed files with 258 additions and 10 deletions

View File

@ -1,26 +1,30 @@
package org.thoughtcrime.securesms.util package org.thoughtcrime.securesms.util
import android.content.ContentResolver
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.DialogInterface.OnClickListener import android.content.DialogInterface.OnClickListener
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.text.TextUtils import android.text.TextUtils
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import network.loki.messenger.R 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.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.mms.PartAuthority
import org.session.libsession.utilities.task.ProgressDialogAsyncTask
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import kotlin.jvm.Throws import java.util.*
import java.util.concurrent.TimeUnit
import org.session.libsession.utilities.Util
/** /**
* Saves attachment files to an external storage using [MediaStore] API. * Saves attachment files to an external storage using [MediaStore] API.
@ -93,13 +97,14 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
} }
} }
@Throws(IOException::class) /*@Throws(IOException::class)
private fun saveAttachment(context: Context, attachment: Attachment): String? { private fun saveAttachment(context: Context, attachment: Attachment): String? {
val resolver = context.contentResolver val resolver = context.contentResolver
val contentType = MediaUtil.getCorrectedMimeType(attachment.contentType)!! val contentType = MediaUtil.getCorrectedMimeType(attachment.contentType)!!
val fileName = attachment.fileName val fileName = if (attachment.fileName != null) sanitizeOutputFileName(attachment.fileName)
?: sanitizeOutputFileName(generateOutputFileName(contentType, attachment.date)) else sanitizeOutputFileName(generateOutputFileName(contentType, attachment.date))
val mediaRecord = ContentValues() val mediaRecord = ContentValues()
val mediaVolume = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { val mediaVolume = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
@ -121,10 +126,14 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
} }
contentType.startsWith("audio/") -> { contentType.startsWith("audio/") -> {
collectionUri = MediaStore.Audio.Media.getContentUri(mediaVolume) collectionUri = MediaStore.Audio.Media.getContentUri(mediaVolume)
mediaRecord.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) mediaRecord.put(MediaStore.Audio.Media.TITLE, "test")
mediaRecord.put(MediaStore.Audio.Media.DISPLAY_NAME, "test")
mediaRecord.put(MediaStore.Audio.Media.MIME_TYPE, contentType) mediaRecord.put(MediaStore.Audio.Media.MIME_TYPE, contentType)
mediaRecord.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis()) mediaRecord.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
mediaRecord.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis()) mediaRecord.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis())
val directory = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)
mediaRecord.put(MediaStore.Audio.Media.DATA, String.format("%s/%s", directory, fileName))
//collectionUri = directory?.toUri()!!
} }
contentType.startsWith("image/") -> { contentType.startsWith("image/") -> {
@ -160,6 +169,124 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
} }
return mediaFileUri.toString() return mediaFileUri.toString()
}*/
@Throws(IOException::class)
private fun saveAttachment(context: Context, attachment: Attachment): String? {
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)
}
}
}
}
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
}
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)
}
if (outputFile.isHidden) {
throw IOException("Specified name would not be visible")
}
return Uri.fromFile(outputFile)
} else {
var outputFileName = fileName
var dataPath = java.lang.String.format("%s/%s", getMediaStoreContentUriForType(contentType), outputFileName)
var i = 0
while (pathTaken(outputUri, dataPath)) {
Log.d(TAG, "The content exists. Rename and check again.")
outputFileName = base + "-" + ++i + "." + extension
dataPath = java.lang.String.format("%s/%s", getMediaStoreContentUriForType(contentType), outputFileName)
}
contentValues.put(MediaStore.MediaColumns.DATA, dataPath)
}
return context.contentResolver.insert(outputUri, contentValues)
}
private fun getFileNameParts(fileName: String): Array<String> {
val tokens = fileName.split("\\.(?=[^\\.]+$)".toRegex()).toTypedArray()
return arrayOf(tokens[0], if (tokens.size > 1) tokens[1] else "")
}
private fun getExternalPathToFileForType(contentType: String): String {
val storage: File = when {
contentType.startsWith("video/") ->
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
contentType.startsWith("audio/") ->
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
contentType.startsWith("image/") ->
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
else ->
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
}
return storage.absolutePath
}
@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()
}
} }
private fun generateOutputFileName(contentType: String, timestamp: Long): String { private fun generateOutputFileName(contentType: String, timestamp: Long): String {
@ -177,8 +304,7 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
override fun onPostExecute(result: Pair<Int, String?>) { override fun onPostExecute(result: Pair<Int, String?>) {
super.onPostExecute(result) super.onPostExecute(result)
val context = contextReference.get() val context = contextReference.get() ?: return
if (context == null) return
when (result.first) { when (result.first) {
RESULT_FAILURE -> { RESULT_FAILURE -> {

View File

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

View File

@ -1,7 +1,10 @@
package org.session.libsignal.utilities.externalstorage package org.session.libsignal.utilities.externalstorage
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import java.io.File import java.io.File
object ExternalStorageUtil { object ExternalStorageUtil {
@ -45,6 +48,30 @@ object ExternalStorageUtil {
return context.externalCacheDir 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 @JvmStatic
fun getCleanFileName(fileName: String?): String? { fun getCleanFileName(fileName: String?): String? {
var fileName = fileName ?: return null var fileName = fileName ?: return null