Fix picking directory on some devices

This commit is contained in:
Koitharu
2025-02-15 11:24:31 +02:00
parent d558c2fcc0
commit 129035bda3
8 changed files with 117 additions and 31 deletions

View File

@@ -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<Uri?>
) : ActivityResultLauncher<Uri?>() {
constructor(activityResultCaller: ActivityResultCaller, callback: ActivityResultCallback<Uri?>) : 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<Uri?, *>
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
}
}
}

View File

@@ -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<DialogImportBinding>(), View.On
private val importFileCall = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
startImport(it)
}
private val importDirCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
private val importDirCall = OpenDocumentTreeHelper(this) {
startImport(listOfNotNull(it))
}

View File

@@ -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<Uri?> {
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<Uri>? = null

View File

@@ -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)
}

View File

@@ -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<PeriodicalBackupSettingsViewModel>()
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) {

View File

@@ -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<DialogDirectorySelectBind
OnListItemClickListener<DirectoryModel> {
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(

View File

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

View File

@@ -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<ActivityMangaDirectoriesBinding>()
OnListItemClickListener<DirectoryModel>, 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(