From 129035bda3f1d61072bac4c9caf48e7bbf5eb636 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 15 Feb 2025 11:24:31 +0200 Subject: [PATCH] Fix picking directory on some devices --- .../kotatsu/core/os/OpenDocumentTreeHelper.kt | 92 +++++++++++++++++++ .../kotatsu/local/ui/ImportDialogFragment.kt | 3 +- .../kotatsu/reader/ui/PageSaveHelper.kt | 5 +- .../settings/DownloadsSettingsFragment.kt | 4 +- .../PeriodicalBackupSettingsFragment.kt | 5 +- .../storage/MangaDirectorySelectDialog.kt | 9 +- .../settings/storage/PickDirectoryContract.kt | 20 ---- .../directories/MangaDirectoriesActivity.kt | 10 +- 8 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/PickDirectoryContract.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt new file mode 100644 index 000000000..8a9dc368e --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt @@ -0,0 +1,92 @@ +package org.koitharu.kotatsu.core.os + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.storage.StorageManager +import android.provider.DocumentsContract +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityOptionsCompat +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug + +// https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr +class OpenDocumentTreeHelper( + activityResultCaller: ActivityResultCaller, + flags: Int, + callback: ActivityResultCallback +) : ActivityResultLauncher() { + + constructor(activityResultCaller: ActivityResultCaller, callback: ActivityResultCallback) : this( + activityResultCaller, + 0, + callback, + ) + + private val pickFileTreeLauncherQ = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activityResultCaller.registerForActivityResult(OpenDocumentTreeContractQ(flags), callback) + } else { + null + } + private val pickFileTreeLauncherLegacy = activityResultCaller.registerForActivityResult( + contract = OpenDocumentTreeContractLegacy(flags), + callback = callback, + ) + + override fun launch(input: Uri?, options: ActivityOptionsCompat?) { + if (pickFileTreeLauncherQ == null) { + pickFileTreeLauncherLegacy.launch(input, options) + return + } + try { + pickFileTreeLauncherQ.launch(input, options) + } catch (e: Exception) { + e.printStackTraceDebug() + pickFileTreeLauncherLegacy.launch(input, options) + } + } + + override fun unregister() { + pickFileTreeLauncherQ?.unregister() + pickFileTreeLauncherLegacy.unregister() + } + + override val contract: ActivityResultContract + get() = pickFileTreeLauncherQ?.contract ?: pickFileTreeLauncherLegacy.contract + + private open class OpenDocumentTreeContractLegacy( + private val flags: Int, + ) : ActivityResultContracts.OpenDocumentTree() { + + override fun createIntent(context: Context, input: Uri?): Intent { + val intent = super.createIntent(context, input) + intent.addFlags(flags) + return intent + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + private class OpenDocumentTreeContractQ( + private val flags: Int, + ) : OpenDocumentTreeContractLegacy(flags) { + + override fun createIntent(context: Context, input: Uri?): Intent { + val intent = (context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager) + ?.primaryStorageVolume + ?.createOpenDocumentTreeIntent() + if (intent == null) { // fallback + return super.createIntent(context, input) + } + intent.addFlags(flags) + if (input != null) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input) + } + return intent + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt index 2deb31ee0..08b85061e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt @@ -10,6 +10,7 @@ import androidx.activity.result.contract.ActivityResultContracts import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.databinding.DialogImportBinding @@ -25,7 +26,7 @@ class ImportDialogFragment : AlertDialogFragment(), View.On private val importFileCall = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { startImport(it) } - private val importDirCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { + private val importDirCall = OpenDocumentTreeHelper(this) { startImport(listOfNotNull(it)) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt index 7c9db528e..d2b393a3f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt @@ -5,7 +5,6 @@ import android.net.Uri import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts import androidx.core.net.toFile import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile @@ -27,6 +26,7 @@ import okio.openZip import okio.sink import okio.source import org.koitharu.kotatsu.core.image.BitmapDecoderCompat +import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.MimeTypes import org.koitharu.kotatsu.core.util.ext.isFileUri @@ -52,8 +52,7 @@ class PageSaveHelper @AssistedInject constructor( ) : ActivityResultCallback { private val savePageRequest = activityResultCaller.registerForActivityResult(PageSaveContract(), this) - private val pickDirectoryRequest = - activityResultCaller.registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), this) + private val pickDirectoryRequest = OpenDocumentTreeHelper(activityResultCaller, this) private var continuation: CancellableContinuation? = null diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt index 1df4e1d4f..1afb711b7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt @@ -5,7 +5,6 @@ import android.content.SharedPreferences import android.net.Uri import android.os.Bundle import android.view.View -import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile import androidx.preference.ListPreference import androidx.preference.Preference @@ -16,6 +15,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.nav.router +import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.DownloadFormat import org.koitharu.kotatsu.core.prefs.TriStateOption @@ -44,7 +44,7 @@ class DownloadsSettingsFragment : @Inject lateinit var downloadsScheduler: DownloadWorker.Scheduler - private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { + private val pickFileTreeLauncher = OpenDocumentTreeHelper(this) { if (it != null) onDirectoryPicked(it) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt index 839b2ecf0..37262769a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.text.format.DateUtils import android.view.View import androidx.activity.result.ActivityResultCallback -import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.preference.EditTextPreference import androidx.preference.Preference @@ -16,6 +15,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.backup.TelegramBackupUploader import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.nav.router +import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.util.ext.observe @@ -34,7 +34,7 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi private val viewModel by viewModels() - private val outputSelectCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), this) + private val outputSelectCall = OpenDocumentTreeHelper(this, this) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_backup_periodic) @@ -60,6 +60,7 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi viewModel.checkTelegram() true } + else -> return super.onPreferenceTreeClick(preference) } if (!result) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt index ecf5b0e78..e7fd70bbf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/MangaDirectorySelectDialog.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.settings.storage import android.Manifest +import android.content.Intent import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -14,6 +15,7 @@ import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.ToastErrorObserver +import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.observe @@ -26,7 +28,12 @@ class MangaDirectorySelectDialog : AlertDialogFragment { private val viewModel: MangaDirectorySelectViewModel by viewModels() - private val pickFileTreeLauncher = registerForActivityResult(PickDirectoryContract()) { + private val pickFileTreeLauncher = OpenDocumentTreeHelper( + activityResultCaller = this, + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION, + ) { if (it != null) viewModel.onCustomDirectoryPicked(it) } private val permissionRequestLauncher = registerForActivityResult( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/PickDirectoryContract.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/PickDirectoryContract.kt deleted file mode 100644 index af04bb8e8..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/PickDirectoryContract.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.koitharu.kotatsu.settings.storage - -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.activity.result.contract.ActivityResultContracts - -//FIXME: https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr -class PickDirectoryContract : ActivityResultContracts.OpenDocumentTree() { - - override fun createIntent(context: Context, input: Uri?): Intent { - val intent = super.createIntent(context, input) - intent.addFlags( - Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION, - ) - return intent - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesActivity.kt index 31233e965..297537f50 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/storage/directories/MangaDirectoriesActivity.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.settings.storage.directories import android.Manifest +import android.content.Intent import android.os.Build import android.os.Bundle import android.view.View @@ -16,6 +17,7 @@ import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.observe @@ -24,7 +26,6 @@ import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.databinding.ActivityMangaDirectoriesBinding import org.koitharu.kotatsu.settings.storage.DirectoryDiffCallback import org.koitharu.kotatsu.settings.storage.DirectoryModel -import org.koitharu.kotatsu.settings.storage.PickDirectoryContract import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract @AndroidEntryPoint @@ -32,7 +33,12 @@ class MangaDirectoriesActivity : BaseActivity() OnListItemClickListener, View.OnClickListener { private val viewModel: MangaDirectoriesViewModel by viewModels() - private val pickFileTreeLauncher = registerForActivityResult(PickDirectoryContract()) { + private val pickFileTreeLauncher = OpenDocumentTreeHelper( + activityResultCaller = this, + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION, + ) { if (it != null) viewModel.onCustomDirectoryPicked(it) } private val permissionRequestLauncher = registerForActivityResult(