diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt
index 1f05978ba68c..74af8da26767 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt
@@ -35,6 +35,7 @@ import com.ichi2.anki.exception.StorageAccessException
import com.ichi2.anki.servicelayer.PreferenceUpgradeService
import com.ichi2.anki.servicelayer.PreferenceUpgradeService.setPreferencesUpToDate
import com.ichi2.anki.servicelayer.ScopedStorageService.isLegacyStorage
+import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.ui.windows.permissions.InternetPermissionFragment
import com.ichi2.anki.ui.windows.permissions.NotificationsPermissionFragment
import com.ichi2.anki.ui.windows.permissions.PermissionsFragment
@@ -257,6 +258,9 @@ internal fun selectAnkiDroidFolder(
}
fun selectAnkiDroidFolder(context: Context): AnkiDroidFolder {
+ if (Prefs.usePrivateStorage) {
+ return AnkiDroidFolder.AppPrivateFolder
+ }
val canAccessLegacyStorage = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || Environment.isExternalStorageLegacy()
val currentFolderIsAccessibleAndLegacy = canAccessLegacyStorage && isLegacyStorage(context, setCollectionPath = false) == true
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
index 12039f0692dc..6a12806902a6 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
@@ -349,14 +349,18 @@ class IntentHandler : AbstractIntentHandler() {
*
* @throws SystemStorageException if `getExternalFilesDir` returns null
*/
+
+ fun hasRequiredPermissions(context: Context): Boolean {
+ if (Prefs.usePrivateStorage) return true
+ return !ScopedStorageService.isLegacyStorage(context) || hasLegacyStorageAccessPermission(context) ||
+ Permissions.isExternalStorageManagerCompat()
+ }
+
fun grantedStoragePermissions(
context: Context,
showToast: Boolean,
): Boolean {
- val granted =
- !ScopedStorageService.isLegacyStorage(context) ||
- hasLegacyStorageAccessPermission(context) ||
- Permissions.isExternalStorageManagerCompat()
+ val granted = hasRequiredPermissions(context)
if (!granted && showToast) {
showThemedToast(context, context.getString(R.string.intent_handler_failed_no_storage_permission), false)
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
index 1c148724c78e..9452126dfa7c 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
@@ -339,6 +339,8 @@ open class PrefsRepository(
var internetPermissionRequested by booleanPref(R.string.internet_permission_requested_key, false)
+ var usePrivateStorage by booleanPref(R.string.use_private_storage_key, false)
+
// **************************************** Reviewer **************************************** //
val ignoreDisplayCutout by booleanPref(R.string.ignore_display_cutout_key, false)
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivity.kt
index a5018c44d234..efaec332aa6b 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivity.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivity.kt
@@ -21,19 +21,23 @@ import android.os.Bundle
import android.os.Parcelable
import androidx.activity.addCallback
import androidx.core.content.IntentCompat
+import androidx.core.content.edit
import androidx.fragment.app.commit
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.ichi2.anki.AnkiActivity
+import com.ichi2.anki.AnkiDroidApp
+import com.ichi2.anki.CollectionHelper
import com.ichi2.anki.PermissionSet
import com.ichi2.anki.R
import com.ichi2.anki.databinding.ActivityPermissionsBinding
+import com.ichi2.anki.introduction.hasCollectionStoragePermissions
+import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.showThemedToast
-import com.ichi2.anki.ui.windows.permissions.PermissionsFragment.Companion.HAS_ALL_PERMISSIONS_KEY
-import com.ichi2.anki.ui.windows.permissions.PermissionsFragment.Companion.PERMISSIONS_FRAGMENT_RESULT_KEY
-import com.ichi2.anki.utils.ext.setFragmentResultListener
import com.ichi2.themes.Themes
import com.ichi2.themes.setTransparentStatusBar
import dev.androidbroadcast.vbpd.viewBinding
import timber.log.Timber
+import java.io.File
/**
* Screen responsible for getting permissions from the user.
@@ -60,7 +64,13 @@ class PermissionsActivity : AnkiActivity(R.layout.activity_permissions) {
Themes.setTheme(this)
setTransparentStatusBar()
- binding.continueButton.setOnClickListener { finish() }
+ binding.continueButton.setOnClickListener {
+ if (!hasCollectionStoragePermissions()) {
+ showPrivateStorageWarningDialog()
+ } else {
+ finish()
+ }
+ }
// #20881: Activity should not be launchd without extras
val permissionSet = IntentCompat.getParcelableExtra(intent, PERMISSIONS_SET_EXTRA, PermissionSet::class.java)
@@ -75,10 +85,6 @@ class PermissionsActivity : AnkiActivity(R.layout.activity_permissions) {
requireNotNull(permissionSet.permissionsFragment?.getDeclaredConstructor()?.newInstance()) {
"invalid permissionsFragment"
}
- setFragmentResultListener(PERMISSIONS_FRAGMENT_RESULT_KEY) { _, bundle ->
- val hasAllPermissions = bundle.getBoolean(HAS_ALL_PERMISSIONS_KEY)
- setContinueButtonEnabled(hasAllPermissions)
- }
supportFragmentManager.commit {
replace(R.id.fragment_container, permissionsFragment)
@@ -87,8 +93,26 @@ class PermissionsActivity : AnkiActivity(R.layout.activity_permissions) {
onBackPressedDispatcher.addCallback {}
}
- fun setContinueButtonEnabled(isEnabled: Boolean) {
- binding.continueButton.isEnabled = isEnabled
+ // TODO Rethink the UI - selection UI is preferred over a warning dialog: #21049
+ private fun showPrivateStorageWarningDialog() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.private_storage_warning_title)
+ .setMessage(R.string.private_storage_warning)
+ .setPositiveButton(R.string.dialog_continue) { _, _ ->
+ Prefs.usePrivateStorage = true
+ val externalFilesDir = getExternalFilesDir(null) ?: filesDir
+ val privateDir = File(externalFilesDir, "AnkiDroid")
+ if (!privateDir.mkdirs() && !privateDir.exists()) {
+ Timber.w("Failed to create AnkiDroid directory: ${privateDir.absolutePath}")
+ showThemedToast(this, R.string.something_wrong, false)
+ return@setPositiveButton
+ }
+ AnkiDroidApp.sharedPrefs().edit(commit = true) {
+ putString(CollectionHelper.PREF_COLLECTION_PATH, privateDir.absolutePath)
+ }
+ finish()
+ }.setNegativeButton(R.string.dialog_cancel, null)
+ .show()
}
companion object {
diff --git a/AnkiDroid/src/main/res/layout/activity_permissions.xml b/AnkiDroid/src/main/res/layout/activity_permissions.xml
index 06e3c91e766e..bb788029da89 100644
--- a/AnkiDroid/src/main/res/layout/activity_permissions.xml
+++ b/AnkiDroid/src/main/res/layout/activity_permissions.xml
@@ -45,7 +45,7 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingVertical="12dp"
- android:text="@string/dialog_continue"
+ android:text="@string/dialog_skip"
app:layout_constraintBottom_toBottomOf="parent"
/>
diff --git a/AnkiDroid/src/main/res/values/01-core.xml b/AnkiDroid/src/main/res/values/01-core.xml
index 02b502c57937..94a7a49e72af 100644
--- a/AnkiDroid/src/main/res/values/01-core.xml
+++ b/AnkiDroid/src/main/res/values/01-core.xml
@@ -190,7 +190,7 @@
The new collection will be deleted from your phone if you uninstall AnkiDroid
- AnkiDroid needs some permissions to work
+ AnkiDroid works better with these permissions
AnkiDroid works best with these permissions
Storage access
Saves your collection in a safe place that will not be deleted if the app is uninstalled
diff --git a/AnkiDroid/src/main/res/values/03-dialogs.xml b/AnkiDroid/src/main/res/values/03-dialogs.xml
index f573e791ed8f..5acd750bc14c 100644
--- a/AnkiDroid/src/main/res/values/03-dialogs.xml
+++ b/AnkiDroid/src/main/res/values/03-dialogs.xml
@@ -69,6 +69,7 @@
Remove
Exit
Continue
+ Skip
Processing…
Create
Delete
@@ -217,6 +218,9 @@
Due to Android privacy changes, your data and automated backups will be deleted from your phone if the app is uninstalled
Due to Android privacy changes, your data and automated backups will be inaccessible if the app is uninstalled
+ Limited storage access
+ Without this permission, your AnkiDroid collection will be stored in app private storage and will be permanently deleted if you uninstall AnkiDroid.
+
If you have deck ordering issues (e.g. ‘10’ appears before ‘2’), replace ‘2’ with ‘02’
diff --git a/AnkiDroid/src/main/res/values/preferences.xml b/AnkiDroid/src/main/res/values/preferences.xml
index 8d535ec9fbd0..c1c1fe6692d9 100644
--- a/AnkiDroid/src/main/res/values/preferences.xml
+++ b/AnkiDroid/src/main/res/values/preferences.xml
@@ -269,4 +269,5 @@
internetPermissionRequested
+ use_private_storage
\ No newline at end of file
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivityTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivityTest.kt
index 960f956a0ed6..5c932ee41969 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivityTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/ui/windows/permissions/PermissionsActivityTest.kt
@@ -48,7 +48,6 @@ class PermissionsActivityTest : RobolectricTest() {
@Test
fun testOnClickingContinueActivityFinishes() {
testActivity { activity ->
- activity.setContinueButtonEnabled(true)
activity.findViewById(R.id.continue_button).performClick()
assertThat("activity is finishing", activity.isFinishing)
}