diff --git a/app/build.gradle b/app/build.gradle index 14a655820..547b97ddd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,7 +154,7 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' } -def canonicalVersionCode = 228 +def canonicalVersionCode = 229 def canonicalVersionName = "1.11.11" def postFixSize = 10 diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index b34fc5a22..c0159d317 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -7,10 +7,7 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.net.Uri -import android.os.AsyncTask -import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.os.* import android.view.ActionMode import android.view.Menu import android.view.MenuItem @@ -323,7 +320,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } private fun shareLogs() { - ShareLogsDialog().show(supportFragmentManager,"Share Logs Dialog") + Permissions.with(this) + .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .maxSdkVersion(Build.VERSION_CODES.P) + .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) + .onAnyDenied { + Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() + } + .onAllGranted { + ShareLogsDialog().show(supportFragmentManager,"Share Logs Dialog") + } + .execute() } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt index 09611e0ef..76d6f6a1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt @@ -1,8 +1,15 @@ package org.thoughtcrime.securesms.preferences +import android.content.ContentResolver +import android.content.ContentValues import android.content.Intent +import android.media.MediaScannerConnection +import android.net.Uri import android.os.Build +import android.os.Environment +import android.provider.MediaStore import android.view.LayoutInflater +import android.webkit.MimeTypeMap import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope @@ -12,9 +19,15 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import network.loki.messenger.BuildConfig import network.loki.messenger.R +import org.session.libsignal.utilities.ExternalStorageUtil import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog -import org.thoughtcrime.securesms.providers.BlobProvider +import org.thoughtcrime.securesms.util.StreamUtil +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit class ShareLogsDialog : BaseDialog() { @@ -39,16 +52,41 @@ class ShareLogsDialog : BaseDialog() { shareJob = lifecycleScope.launch(Dispatchers.IO) { val persistentLogger = ApplicationContext.getInstance(context).persistentLogger try { - val logs = persistentLogger.logs.get() - val fileName = "${Build.MANUFACTURER}-${Build.DEVICE}-API${Build.VERSION.SDK_INT}-v${BuildConfig.VERSION_NAME}.txt" - val logUri = BlobProvider().forData(logs.toByteArray()) - .withFileName(fileName) - .withMimeType("text/plain") - .createForSingleSessionOnDisk(requireContext(),null) + val context = requireContext() + val outputUri: Uri = ExternalStorageUtil.getDownloadUri() + val mediaUri = getExternalFile() + if (mediaUri == null) { + // show toast saying media saved + dismiss() + return@launch + } + + val inputStream = persistentLogger.logs.get().byteInputStream() + val updateValues = ContentValues() + if (outputUri.scheme == ContentResolver.SCHEME_FILE) { + FileOutputStream(mediaUri.path).use { outputStream -> + StreamUtil.copy(inputStream, outputStream) + MediaScannerConnection.scanFile(context, arrayOf(mediaUri.path), arrayOf("text/plain"), 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) { + requireContext().contentResolver.update(mediaUri, updateValues, null, null) + } val shareIntent = Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_STREAM, logUri) + putExtra(Intent.EXTRA_STREAM, mediaUri) + data = mediaUri type = "text/plain" } @@ -62,4 +100,56 @@ class ShareLogsDialog : BaseDialog() { } } + @Throws(IOException::class) + private fun pathTaken(outputUri: Uri, dataPath: String): Boolean { + requireContext().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 getExternalFile(): Uri? { + val context = requireContext() + val base = "${Build.MANUFACTURER}-${Build.DEVICE}-API${Build.VERSION.SDK_INT}-v${BuildConfig.VERSION_NAME}-${System.currentTimeMillis()}" + val extension = "txt" + val fileName = "$base.$extension" + val mimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType("text/plain") + val outputUri: Uri = ExternalStorageUtil.getDownloadUri() + 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 + val externalPath = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)!! + var dataPath = String.format("%s/%s", externalPath, outputFileName) + var i = 0 + while (pathTaken(outputUri, dataPath)) { + outputFileName = base + "-" + ++i + "." + extension + dataPath = String.format("%s/%s", externalPath, outputFileName) + } + contentValues.put(MediaStore.MediaColumns.DATA, dataPath) + } + return context.contentResolver.insert(outputUri, contentValues) + } + + } \ No newline at end of file