diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt index ee1cf128e6..fba4a7cd23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt @@ -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 { 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 + 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 = 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 { + 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) { super.onPostExecute(result) - val context = contextReference.get() - if (context == null) return + val context = contextReference.get() ?: return when (result.first) { RESULT_FAILURE -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/StreamUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/StreamUtil.java new file mode 100644 index 0000000000..32c381c00a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/StreamUtil.java @@ -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; + } +} diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/externalstorage/ExternalStorageUtil.kt b/libsignal/src/main/java/org/session/libsignal/utilities/externalstorage/ExternalStorageUtil.kt index fc6c858fd9..4957045a61 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/externalstorage/ExternalStorageUtil.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/externalstorage/ExternalStorageUtil.kt @@ -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