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
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.
@ -93,13 +97,14 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
}
}
@Throws(IOException::class)
/*@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 fileName = if (attachment.fileName != null) sanitizeOutputFileName(attachment.fileName)
else sanitizeOutputFileName(generateOutputFileName(contentType, attachment.date))
val mediaRecord = ContentValues()
val mediaVolume = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
@ -121,10 +126,14 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
}
contentType.startsWith("audio/") -> {
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.DATE_ADDED, 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/") -> {
@ -160,6 +169,124 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
}
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 {
@ -177,8 +304,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 -> {

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