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