From 3414cdccaa99ed37c08daec3464501885b649da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=A4nge?= Date: Wed, 20 Dec 2023 14:17:43 +0100 Subject: [PATCH] Fix export permissions for API >= 33 Fixes #154 --- app/src/main/AndroidManifest.xml | 35 +++++----- .../ui/notes/AudioNoteActivity.kt | 52 +++++--------- .../ui/notes/BaseNoteActivity.kt | 68 ++++++++----------- .../ui/notes/ChecklistNoteActivity.kt | 51 +++++--------- .../ui/notes/SketchActivity.kt | 67 ++++++------------ .../ui/notes/TextNoteActivity.kt | 48 ++++--------- 6 files changed, 113 insertions(+), 208 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 85e29a0b..ca1c1e59 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,32 +2,30 @@ - + - - + tools:node="remove" /> + android:exported="true" + android:theme="@style/SplashTheme"> @@ -35,8 +33,7 @@ - + android:theme="@style/AppTheme.NoActionBar" /> - + android:screenOrientation="portrait" /> + android:label="@string/title_settings" + android:parentActivityName=".ui.main.MainActivity" /> + android:label="@string/title_help" + android:parentActivityName=".ui.main.MainActivity" /> + android:theme="@style/AppTheme.NoActionBar" /> - diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt index b0b73e0a..a6023e7f 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt @@ -19,7 +19,7 @@ import android.content.pm.PackageManager import android.media.AudioManager import android.media.MediaPlayer import android.media.MediaRecorder -import android.media.MediaScannerConnection +import android.os.Build import android.os.Bundle import android.os.Handler import android.util.Log @@ -31,7 +31,6 @@ import android.widget.ImageButton import android.widget.SeekBar import android.widget.SeekBar.OnSeekBarChangeListener import android.widget.TextView -import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider @@ -40,9 +39,8 @@ import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note import java.io.File import java.io.FileInputStream -import java.io.FileOutputStream import java.io.IOException -import java.nio.channels.FileChannel +import java.io.OutputStream /** * Activity that allows to add, edit and delete audio notes. @@ -50,7 +48,7 @@ import java.nio.channels.FileChannel class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { private val btnPlayPause: ImageButton by lazy { findViewById(R.id.btn_play_pause) } private val btnRecord: ImageButton by lazy { findViewById(R.id.btn_record) } - private val tvRecordingTime: TextView by lazy { findViewById(R.id.recording_time) } + private val tvRecordingTime: TextView by lazy { findViewById(R.id.recording_time) } private val seekBar: SeekBar by lazy { findViewById(R.id.seekbar) } private var mRecorder: MediaRecorder? = null @@ -147,11 +145,13 @@ class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { } else { stopRecording() } + R.id.btn_play_pause -> if (!playing) { startPlaying() } else { pausePlaying() } + else -> {} } } @@ -271,40 +271,20 @@ class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { return ActionResult(true, Note(name, mFileName, DbContract.NoteEntry.TYPE_AUDIO, category)) } - override fun onSaveExternalStorage(basePath: File, name: String) { - val file = File(basePath, "/$name.aac") - try { - // Make sure the directory exists. - if (basePath.exists() || basePath.mkdirs()) { - var source: FileChannel? = null - var destination: FileChannel? = null - try { - source = FileInputStream(File(mFilePath)).channel - destination = FileOutputStream(file).channel - destination.transferFrom(source, 0, source.size()) - } finally { - source?.close() - destination?.close() - } + override fun getFileExtension() = ".aac" + override fun getMimeType() = "audio/mp4a-latm" - // Tell the media scanner about the new file so that it is - // immediately available to the user. - MediaScannerConnection.scanFile( - this, arrayOf(file.toString()), null - ) { path, uri -> - Log.i("ExternalStorage", "Scanned $path:") - Log.i("ExternalStorage", "-> uri=$uri") + override fun onSaveExternalStorage(outputStream: OutputStream) { + FileInputStream(File(mFilePath)).use { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + it.transferTo(outputStream) + } else { + val buffer = ByteArray(8192) + var length: Int + while (it.read(buffer).also { length = it } != -1) { + outputStream.write(buffer, 0, length) } - Toast.makeText( - applicationContext, - String.format(getString(R.string.toast_file_exported_to), file.absolutePath), - Toast.LENGTH_LONG - ).show() } - } catch (e: IOException) { - // Unable to create file, likely because external storage is - // not currently mounted. - Log.w("ExternalStorage", "Error writing $file", e) } } } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt index b604756b..3668bd52 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt @@ -26,7 +26,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle -import android.os.Environment import android.preference.PreferenceManager import android.provider.Settings import android.view.Menu @@ -34,6 +33,7 @@ import android.view.MenuItem import android.view.View import android.view.WindowManager import android.widget.* +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat @@ -51,7 +51,7 @@ import org.secuso.privacyfriendlynotes.ui.helper.NotificationHelper.addNotificat import org.secuso.privacyfriendlynotes.ui.helper.NotificationHelper.removeNotificationFromAlarmManager import org.secuso.privacyfriendlynotes.ui.helper.NotificationHelper.showAlertScheduledToast import org.secuso.privacyfriendlynotes.ui.manageCategories.ManageCategoriesActivity -import java.io.File +import java.io.OutputStream import java.util.* @@ -96,7 +96,10 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli protected abstract fun noteToSave(name: String, category: Int): ActionResult protected abstract fun updateNoteToSave(name: String, category: Int): ActionResult protected abstract fun onLoadActivity() - protected abstract fun onSaveExternalStorage(basePath: File, name: String) + protected abstract fun onSaveExternalStorage(outputStream: OutputStream) + + protected abstract fun getFileExtension(): String + protected abstract fun getMimeType(): String protected abstract fun shareNote(name: String): ActionResult protected abstract fun onNoteLoadedFromDB(note: Note) protected abstract fun onNewNote() @@ -293,17 +296,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli R.id.action_export -> { saveOrUpdateNote() - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions( - this, - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), - REQUEST_CODE_EXTERNAL_STORAGE - ) - } else { - saveToExternalStorage() - } + saveToExternalStorage() return true } @@ -388,17 +381,6 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { - REQUEST_CODE_EXTERNAL_STORAGE -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - //Save the file - saveToExternalStorage() - } else { - Toast.makeText( - applicationContext, - R.string.toast_need_permission_write_external, - Toast.LENGTH_LONG - ).show() - } - REQUEST_CODE_AUDIO -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { //Do nothing. App should work } else { @@ -525,23 +507,31 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } } - private fun saveToExternalStorage() { - val state = Environment.getExternalStorageState() - if (Environment.MEDIA_MOUNTED == state) { - val path = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), - "/PrivacyFriendlyNotes" - ) - onSaveExternalStorage(path, etName.text.toString()) - } else { - Toast.makeText( - applicationContext, - R.string.toast_external_storage_not_mounted, - Toast.LENGTH_LONG - ).show() + val saveToExternalStorageResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let {uri -> + val fileOutputStream: OutputStream? = contentResolver.openOutputStream(uri) + fileOutputStream?.let { + onSaveExternalStorage(it) + Toast.makeText( + applicationContext, + String.format(getString(R.string.toast_file_exported_to), uri.toString()), + Toast.LENGTH_LONG + ).show() + } + fileOutputStream?.close() + } } } + private fun saveToExternalStorage() { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.putExtra(Intent.EXTRA_TITLE, etName.text.toString() + getFileExtension()) + intent.type = getMimeType() + saveToExternalStorageResultLauncher.launch(intent) + } + private fun generateStandardName(): String { val sp = getSharedPreferences(PreferenceKeys.SP_VALUES, MODE_PRIVATE) val counter = sp.getInt(PreferenceKeys.SP_VALUES_NAMECOUNTER, 1) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt index 16aed7d5..d3ace959 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt @@ -14,15 +14,18 @@ package org.secuso.privacyfriendlynotes.ui.notes import android.content.Intent -import android.media.MediaScannerConnection import android.os.Bundle -import android.util.Log import android.view.ActionMode import android.view.Menu import android.view.MenuItem import android.view.View -import android.widget.* import android.widget.AbsListView.MultiChoiceModeListener +import android.widget.Adapter +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.ListView +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.util.forEach import org.json.JSONArray @@ -33,8 +36,7 @@ import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note import org.secuso.privacyfriendlynotes.ui.util.CheckListAdapter import org.secuso.privacyfriendlynotes.ui.util.CheckListItem -import java.io.File -import java.io.IOException +import java.io.OutputStream import java.io.PrintWriter /** @@ -86,6 +88,7 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI mode.finish() // Action picked, so close the CAB true } + R.id.action_edit -> { val temp = ArrayList() lvItemList.checkedItemPositions.forEach { key, value -> if (value) temp.add(checklistAdapter.getItem(key)) } @@ -116,6 +119,7 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI true } } + else -> false } } @@ -208,35 +212,14 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI return ActionResult(true, Note(name, jsonArray.toString(), DbContract.NoteEntry.TYPE_CHECKLIST, category)) } - override fun onSaveExternalStorage(basePath: File, name: String) { - val file = File(basePath, "/checklist_$name.txt") - try { - // Make sure the directory exists. - if (basePath.exists() || basePath.mkdirs()) { - val out = PrintWriter(file) - out.println(name) - out.println() - out.println(getContentString()) - out.close() - // Tell the media scanner about the new file so that it is - // immediately available to the user. - MediaScannerConnection.scanFile( - this, arrayOf(file.toString()), null - ) { path, uri -> - Log.i("ExternalStorage", "Scanned $path:") - Log.i("ExternalStorage", "-> uri=$uri") - } - Toast.makeText( - applicationContext, - String.format(getString(R.string.toast_file_exported_to), file.absolutePath), - Toast.LENGTH_LONG - ).show() - } - } catch (e: IOException) { - // Unable to create file, likely because external storage is - // not currently mounted. - Log.w("ExternalStorage", "Error writing $file", e) - } + override fun getMimeType() = "text/plain" + + override fun getFileExtension() = ".txt" + + override fun onSaveExternalStorage(outputStream: OutputStream) { + val out = PrintWriter(outputStream) + out.println(getContentString()) + out.close() } private fun getContentString(): String { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt index 3fd94fed..1e1ed318 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt @@ -19,12 +19,9 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix import android.graphics.drawable.BitmapDrawable -import android.media.MediaScannerConnection import android.os.Bundle -import android.util.Log import android.view.View import android.widget.Button -import android.widget.Toast import androidx.core.content.FileProvider import com.simplify.ink.InkView import org.secuso.privacyfriendlynotes.R @@ -36,6 +33,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException +import java.io.OutputStream /** * Activity that allows to add, edit and delete sketch notes. @@ -54,7 +52,7 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { drawView.bitmap.config ) } - + override fun onCreate(savedInstanceState: Bundle?) { setContentView(R.layout.activity_sketch) @@ -116,7 +114,7 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { override fun determineToSave(title: String, category: Int): Pair { val intent = intent return Pair( - sketchLoaded || !drawView.bitmap.sameAs(emptyBitmap()) && -5 != intent.getIntExtra(EXTRA_CATEGORY, -5), + sketchLoaded || !drawView.bitmap.sameAs(emptyBitmap()) && -5 != intent.getIntExtra(EXTRA_CATEGORY, -5), R.string.toast_emptyNote ) } @@ -177,46 +175,25 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { .show() } - override fun onSaveExternalStorage(basePath: File, name: String) { - val file = File(basePath, "/$name.jpeg") - try { - // Make sure the directory exists. - if (basePath.exists() || basePath.mkdirs()) { - val bm = overlay( - BitmapDrawable( - resources, mFilePath - ).bitmap, drawView.bitmap - ) - val canvas = Canvas(bm) - canvas.drawColor(Color.WHITE) - canvas.drawBitmap( - overlay( - BitmapDrawable( - resources, mFilePath - ).bitmap, drawView.bitmap - ), 0f, 0f, null - ) - bm.compress(Bitmap.CompressFormat.JPEG, 100, FileOutputStream(file)) - - // Tell the media scanner about the new file so that it is - // immediately available to the user. - MediaScannerConnection.scanFile( - this, arrayOf(file.toString()), null - ) { path, uri -> - Log.i("ExternalStorage", "Scanned $path:") - Log.i("ExternalStorage", "-> uri=$uri") - } - Toast.makeText( - applicationContext, - String.format(getString(R.string.toast_file_exported_to), file.absolutePath), - Toast.LENGTH_LONG - ).show() - } - } catch (e: IOException) { - // Unable to create file, likely because external storage is - // not currently mounted. - Log.w("ExternalStorage", "Error writing $file", e) - } + override fun getFileExtension() = ".jpeg" + override fun getMimeType() = "image/jpeg" + + override fun onSaveExternalStorage(outputStream: OutputStream) { + val bm = overlay( + BitmapDrawable( + resources, mFilePath + ).bitmap, drawView.bitmap + ) + val canvas = Canvas(bm) + canvas.drawColor(Color.WHITE) + canvas.drawBitmap( + overlay( + BitmapDrawable( + resources, mFilePath + ).bitmap, drawView.bitmap + ), 0f, 0f, null + ) + bm.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) } companion object { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index d8e1420b..53dd51e8 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -17,7 +17,6 @@ import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Typeface -import android.media.MediaScannerConnection import android.os.Bundle import android.text.Html import android.text.Spannable @@ -25,19 +24,17 @@ import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan import android.text.style.UnderlineSpan -import android.util.Log import android.view.View import android.widget.EditText -import android.widget.Toast import androidx.lifecycle.MutableLiveData import com.google.android.material.floatingactionbutton.FloatingActionButton import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note -import java.io.File -import java.io.IOException +import java.io.OutputStream import java.io.PrintWriter + /** * Activity that allows to add, edit and delete text notes. */ @@ -237,6 +234,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { etContent.text = totalText etContent.setSelection(startSelection) } + else -> {} } } @@ -362,36 +360,14 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } } - override fun onSaveExternalStorage(basePath: File, name: String) { - val file = File(basePath, "/text_${name}.txt") - try { - // Make sure the directory exists. - if (basePath.exists() || basePath.mkdirs()) { - val out = PrintWriter(file) - out.println(name) - out.println() - out.println(Html.toHtml(etContent.text)) - out.close() - // Tell the media scanner about the new file so that it is - // immediately available to the user. - MediaScannerConnection.scanFile( - this, arrayOf(file.toString()), null - ) { path, uri -> - Log.i("ExternalStorage", "Scanned $path:") - Log.i("ExternalStorage", "-> uri=$uri") - } - Toast.makeText( - applicationContext, - String.format(getString(R.string.toast_file_exported_to), file.absolutePath), - Toast.LENGTH_LONG - ).show() - } else { - Log.e("file", "${file.exists()} ${file.mkdirs()}") - } - } catch (e: IOException) { - // Unable to create file, likely because external storage is - // not currently mounted. - Log.w("ExternalStorage", "Error writing $file", e) - } + override fun getMimeType() = "text/plain" + + override fun getFileExtension() = ".txt" + + + override fun onSaveExternalStorage(outputStream: OutputStream) { + val out = PrintWriter(outputStream) + out.println(Html.toHtml(etContent.text)) + out.close() } } \ No newline at end of file