Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
12 changes: 8 additions & 4 deletions AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/res/layout/activity_permissions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>

Expand Down
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/res/values/01-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
<string name="new_unsafe_collection">The new collection will be deleted from your phone if you uninstall AnkiDroid</string>

<!-- Permissions screen -->
<string name="permissions_screen_headline">AnkiDroid needs some permissions to work</string>
<string name="permissions_screen_headline">AnkiDroid works better with these permissions</string>
<string name="permissions_screen_optional_headline" comment="Explains that there are optional permissions that can be granted to AnkiDroid.">AnkiDroid works best with these permissions</string>
<string name="storage_access_title">Storage access</string>
<string name="storage_access_summary">Saves your collection in a safe place that will not be deleted if the app is uninstalled</string>
Expand Down
4 changes: 4 additions & 0 deletions AnkiDroid/src/main/res/values/03-dialogs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<string name="dialog_remove">Remove</string>
<string name="dialog_exit" comment="Label for a button which will exit/finish the current screen">Exit</string>
<string name="dialog_continue">Continue</string>
<string name="dialog_skip">Skip</string>
<string name="dialog_processing">Processing…</string>
<string name="dialog_positive_create" comment="Create a new collection, probably erasing the previous one, may be necessary in case of error.">Create</string>
<string name="dialog_positive_delete" maxLength="28">Delete</string>
Expand Down Expand Up @@ -217,6 +218,9 @@
<string name="dismiss_backup_warning_new_user">Due to Android privacy changes, your data and automated backups will be deleted from your phone if the app is uninstalled</string>
<string name="dismiss_backup_warning_upgrade">Due to Android privacy changes, your data and automated backups will be inaccessible if the app is uninstalled</string>

<string name="private_storage_warning_title">Limited storage access</string>
<string name="private_storage_warning">Without this permission, your AnkiDroid collection will be stored in app private storage and will be permanently deleted if you uninstall AnkiDroid.</string>

<string name="create_deck_numeric_hint">If you have deck ordering issues (e.g. ‘10’ appears before ‘2’), replace ‘2’ with ‘02’</string>

<!-- Set Due Date -->
Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/res/values/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,5 @@

<!-- Onboarding -->
<string name="internet_permission_requested_key">internetPermissionRequested</string>
<string name="use_private_storage_key">use_private_storage</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ class PermissionsActivityTest : RobolectricTest() {
@Test
fun testOnClickingContinueActivityFinishes() {
testActivity { activity ->
activity.setContinueButtonEnabled(true)
activity.findViewById<AppCompatButton>(R.id.continue_button).performClick()
assertThat("activity is finishing", activity.isFinishing)
}
Expand Down
Loading