ankidroid / Anki-Android

AnkiDroid: Anki flashcards on Android. Your secret trick to achieve superhuman information retention.
GNU General Public License v3.0
8.66k stars 2.24k forks source link

[BUG] Storage permission is asked in Android 13 and can't be granted #14423

Closed WhtUsrWrks closed 8 months ago

WhtUsrWrks commented 1 year ago

Hi.. I hadn't used ankidroid so my Samsung Galaxy A21 removed its permissions, and I wanted to use it again today but when I open it it says "Grant storage permission", but I've granted all permissions and it still asks. I've tried u installing and reinstalling and it says the same thing. I don't seem to have a dedicated Storage Permission option in permission manager but it has permission for files and that's on... I don't know what else to change? Thank you

snowtimeglass commented 1 year ago
  • collectionPath is set to the default, which I believe would be /AnkiDroid (as Environment.isExternalStorageLegacy is true)

I suppose so, too.

  • This is still unusable for the user, as the user still does not have permission to access it

Can't the video of @Ezkielnet above (and subsequent comments) disprove the prediction?

snowtimeglass commented 1 year ago

If I remember correctly, Delete app data did delete my decks.

Although the conditions are different, the Delete app data doesn't delete the decks (collection) on my device (Android 9).

david-allison commented 1 year ago

Ah: you're correct. currentFolderIsAccessibleAndLegacy would return false after wiping app data, so wiping app data would also work to allow a user to reuse the app without uninstalling (same outcome though: /AnkiDroid is inaccessible, so it appears that decks have been deleted).

https://github.com/ankidroid/Anki-Android/blob/603bd59a20c15037d0eea1297e22178dce6b76a0/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt#L201

For this to occur, the Anki collection should still be intact in ~/AnkiDroid after deleting app data, but since AnkiDroid can no longer access this folder, it appears that a new collection is created from scratch.

snowtimeglass commented 1 year ago

so wiping app data would also work to allow a user to reuse the app without uninstalling (same outcome though: /AnkiDroid is inaccessible, so it appears that decks have been deleted).

the Anki collection should still be intact in ~/AnkiDroid after deleting app data, but since AnkiDroid can no longer access this folder, it appears that a new collection is created from scratch.

Excuse me, the following two thoughts are still not consistent in my mind.

david-allison commented 1 year ago

The new collection is created in /Android/data/com.ichi2.anki/files/AnkiDroid

snowtimeglass commented 1 year ago

It is created automatically without this dialog? image

david-allison commented 1 year ago

Yes, "Create a new collection" will call through to:

https://github.com/ankidroid/Anki-Android/blob/603bd59a20c15037d0eea1297e22178dce6b76a0/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt#L502-L512

/Android/data/com.ichi2.anki/files/AnkiDroid is the new default location of the AnkiDroid folder

https://github.com/ankidroid/Anki-Android/blob/603bd59a20c15037d0eea1297e22178dce6b76a0/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt#L334-L441

snowtimeglass commented 1 year ago

Ah, I think I understand now. Thank you so much for your time and detailed explanation.

chmnchiang commented 1 year ago

Just want to report that "Delete app data" deleted all my decks (and I couldn't sync to web because I can't open the app 🥹)... So probably not a good general solution....

david-allison commented 1 year ago

Just want to report that "Delete app data" deleted all my decks (and I couldn't sync to web because I can't open the app 🥹)... So probably not a good general solution....

Does your phone have an AnkiDroid folder on the root of the device storage?

That should contain your data.

Installing the full APK from GitHub should allow you to access it again

https://github.com/ankidroid/anki-android/releases/latest

chmnchiang commented 1 year ago

That works. Thanks

dimdenGD commented 11 months ago

I got this bug and here's some info, maybe will be useful to anyone (checked on 2.17alpha8 Android 13):

david-allison commented 11 months ago

@dimdenGD that seems to be an instance of the following, which will be possible to resolve:

ben-albrecht commented 11 months ago

I had this issue recently. Uninstalling the app without keeping local data, then reinstalling worked for me. I selected "create a new collection" when opening it after reinstallation.

However, this will wipe your decks if you don't have your collection synced to AnkiWeb.

hanpingchinese commented 11 months ago

Out of interest, as an alternative to installing the universal APK from github, couldn't you suggest the user copies their AnkiDroid folder to /sdcard/Android/data/com.ichi2.anki/files?

david-allison commented 11 months ago

Out of interest, as an alternative to installing the universal APK from github, couldn't you suggest the user copies their AnkiDroid folder to /sdcard/Android/data/com.ichi2.anki/files?

Android file handling has been painful for the past few releases and the user experience would (likely) be much worse:

soshial commented 10 months ago

Out of interest, why doesn't AnkiDroid simply store decks in its own folder, for which there would be no permissions required. Asking for generic STORAGE permission just to store and manage own files is a bad approach for an app.

mikehardy commented 10 months ago

@soshial you first assume that AnkiDroid doesn't store decks in it's own folder.

That is an incorrect assumption for current AnkiDroid, so the rest of your comment is invalid.

For older versions (which were released way back with Android 1!) there was no "own folder". So AnkiDroid used /sdcard/AnkiDroid.

Now, what do you do as an app when you have users from older versions of Android and AnkiDroid that still have their precious data in /sdcard/AnkiDroid but your new version uses it's own folder? Why you migrate the data of course!

Which was a huge programming effort. But it's done now.

However, to do that migration you still need access to /sdcard/AnkiDroid right? Now we're all caught up :-)

TheMadSword commented 10 months ago

Same issue @ Pixel 4a, Android 13, app wouldn't boot. When installing latest 2.17alpha14, I then get this screen when booting :

image

What is weird : 1- I don't recall ever having that specific permission screen before on my phone. 2- Whenever the toggle is used, dragging or clicking, it directly get back to "off". It cannot be activated. 3- All permissions [when going to "App permissions" in my app settings] I can give on my phone to AnkiDroid are given. The closest seems to be it is "Read and write to the AnkiDroid database". Others are Microphone and notifications...

Thanks !

soshial commented 10 months ago

That's great news that AnkiDroid will use internal folder for deck files @mikehardy. Thanks for the explanation.

Although v2.16 cannot be launched for me at all (I couldn't install latest 2.17alpha versions because of errors). When I open the app on my Android 14 it redirects me to the permissions screen and demands to allow storage permission, which is not in the list. @mikehardy

mikehardy commented 10 months ago

Use the full/universal build from our releases page. You say "errors" but don't specifically say what error. We're not aware of any thing that would stop am install of the alpha so that should work. Be more specific and we can probably help

jgarvin commented 9 months ago

Can confirm this still exists. I got the notice today it had been too long since login and my decks could get deleted, so I opened Ankidroid to try to get it to sync. It asks for storage permission and opens android app settings for Ankidroid. However the only permission listed is permission to "read and write to and from the Ankidroid database", which I granted, but the message still appears. Under "Not allowed" permissions it says "No permissions denied."

mikehardy commented 9 months ago

@jgarvin how did it go following this direction?

Use the full/universal build from our releases page

...hopefully that worked

soshial commented 9 months ago

@mikehardy I wonder how this universal build differs from the one in the Google Play? Why cannot this build be published on Google Play then, if I may ask?

david-allison commented 9 months ago

@mikehardy I wonder how this universal build differs from the one in the Google Play? Why cannot this build be published on Google Play then, if I may ask?

Google Play won't allow it, as they don't want apps to have access to the filesystem. https://support.google.com/googleplay/android-developer/answer/10467955?hl=en

Feel free to inspect the source for our changes for the play/full version. mainly:

https://github.com/ankidroid/Anki-Android/blob/56a195bf466c57e5a848e6ba8796a529816d5cab/AnkiDroid/src/play/AndroidManifest.xml#L1-L14

alexanderadam commented 9 months ago

Google Play won't allow it, as they don't want apps to have access to the filesystem. https://support.google.com/googleplay/android-developer/answer/10467955?hl=en

And why is the F-Droid release buggy as well then? :thinking:

And how do other apps access data then? For instance the Video player NOVA can access data from various paths. And the same is true for VLC player of course.

Furthermore I have PDF viewers and other document apps that can access files. What's the difference here? :thinking:

david-allison commented 9 months ago

Google Play won't allow it, as they don't want apps to have access to the filesystem. https://support.google.com/googleplay/android-developer/answer/10467955?hl=en

And why is the F-Droid release buggy as well then? 🤔

This occurs in the F-Droid release? Could you post a separate issue

david-allison commented 9 months ago

They either use DocumentFile + requesting access to a folder, which is insufficient for database access, or they use MediaStore, which is also insufficient for database access

alexanderadam commented 9 months ago

This occurs in the F-Droid release? Could you post a separate issue

done :+1:

brishtibheja commented 8 months ago

Weird. It is working fine with my Xiaomi device. I just simply used "set up my device" from Google and shared the app from my old phone using ShareMe. I was using arm64 ver. from here. It was the second beta version. Maybe that's why it didn't asked me for permissions. People who are having problems may try syncing settings from a old device using their Google account.

schoeneicher commented 8 months ago

Same problem with 2.17beta6 on Galaxy Note20 Android 13. Cannot install apk because company managed phone.

david-allison commented 8 months ago

Partial patch if anyone wants to take this to completion. The outcome is awful, but our hands are tied. The main solace is that there's no data deletion, only revocation of AnkiDroid's access to the data.

TL;DR: If a user is on Tiramisu (Android 13/SDK 33) or above, Android can revoke WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE and the only way to reapply this is adb

Options:

Patch ```patch Subject: [PATCH] AndroidPermanentlyRevokedPermissionsDialog.kt --- Index: AnkiDroid/src/main/res/values/01-core.xml IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/AnkiDroid/src/main/res/values/01-core.xml b/AnkiDroid/src/main/res/values/01-core.xml --- a/AnkiDroid/src/main/res/values/01-core.xml +++ b/AnkiDroid/src/main/res/values/01-core.xml @@ -274,6 +274,7 @@ Inaccessible collection We are unable to access your collection after AnkiDroid is uninstalled due to a change in Play Store Policy\n\nYour data is safe and can be restored. It is located at\n%s\n\nSelect an option below to restore: + We are unable to access your collection after AnkiDroid is uninstalled due to a change in Play Store Policy\n\nYour data is safe and can be restored. It is located at\n%s\n\nSelect an option below to restore: Restore from AnkiWeb (recommended) Restore folder access (recommended) Restore folder access (advanced) Index: AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/AndroidPermanentlyRevokedPermissionsDialog.kt =================================================================== diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/AndroidPermanentlyRevokedPermissionsDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/AndroidPermanentlyRevokedPermissionsDialog.kt new file mode 100644 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/AndroidPermanentlyRevokedPermissionsDialog.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.anki.ui.windows.permissions + +import android.annotation.SuppressLint +import androidx.annotation.StringRes +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.listItems +import com.ichi2.anki.AnkiActivity +import com.ichi2.anki.R +import com.ichi2.anki.dialogs.DatabaseErrorDialog +import com.ichi2.anki.dialogs.DatabaseErrorDialog.* + +object AndroidPermanentlyRevokedPermissionsDialog { + @SuppressLint("CheckResult") + fun show(context: AnkiActivity) { + val listItemData = StoragePermanentlyRevokedOptions.createList() + + val info = """Android 13 may permanently removes access to your AnkiDroid collection if + |AnkiDroid is installed from Google Play + | + |Your data is safe, but inaccessible + | + | + """.trimMargin() + MaterialDialog(context).show { + message(res = R.string.directory_revoked_after_inactivity) + listItems(items = listItemData.map { context.getString(it.stringRes) }, waitForPositiveButton = false) { _: MaterialDialog, index: Int, _: CharSequence -> + val listItem = listItemData[index] + listItem.onClick(context) + if (listItem.dismissesDialog) { + this.dismiss() + } + } + noAutoDismiss() + cancelable(false) + } + } + + /** + * List items, copied from [DatabaseErrorDialog.UninstallListItem] + */ + private class StoragePermanentlyRevokedOptions(@StringRes val stringRes: Int, val dismissesDialog: Boolean, val onClick: (AnkiActivity) -> Unit) { + companion object { + fun createList(): List { + return UninstallListItem.createList() + .filter { it != UninstallListItem.RESTORE_FROM_ANKIWEB } + .map { listItem -> + StoragePermanentlyRevokedOptions( + stringRes = listItem.stringRes, + dismissesDialog = listItem.dismissesDialog, + onClick = listItem.onClick + ) + } + } + } + } +} \ No newline at end of file Index: AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DatabaseErrorDialog.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DatabaseErrorDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DatabaseErrorDialog.kt --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DatabaseErrorDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/DatabaseErrorDialog.kt @@ -364,7 +364,7 @@ } /** List items for [DIALOG_STORAGE_UNAVAILABLE_AFTER_UNINSTALL] */ - private enum class UninstallListItem(@StringRes val stringRes: Int, val dismissesDialog: Boolean, val onClick: (DeckPicker) -> Unit) { + enum class UninstallListItem(@StringRes val stringRes: Int, val dismissesDialog: Boolean, val onClick: (AnkiActivity) -> Unit) { RESTORE_FROM_ANKIWEB( R.string.restore_data_from_ankiweb, @@ -392,7 +392,8 @@ RESTORE_FROM_BACKUP( R.string.restore_data_from_backup, dismissesDialog = false, - { deckPicker -> + { activity -> + val deckPicker = activity as DeckPicker Timber.i("Restoring from colpkg") val newAnkiDroidDirectory = CollectionHelper.getDefaultAnkiDroidDirectory(deckPicker) deckPicker.importColpkgListener = DatabaseRestorationListener(deckPicker, newAnkiDroidDirectory) @@ -429,7 +430,7 @@ companion object { /** A dialog which creates a new collection in an unsafe location */ - fun displayResetToNewDirectoryDialog(context: DeckPicker) { + fun displayResetToNewDirectoryDialog(context: AnkiActivity) { AlertDialog.Builder(context).show { title(R.string.backup_new_collection) setIcon(R.drawable.ic_warning) Index: AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt @@ -15,9 +15,11 @@ */ package com.ichi2.anki.ui.windows.permissions +import android.os.Build import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts +import com.ichi2.anki.AnkiActivity import com.ichi2.anki.R import com.ichi2.utils.Permissions import com.ichi2.utils.hasAnyOfPermissionsBeenDenied @@ -44,5 +46,13 @@ showToastAndOpenAppSettingsScreen(R.string.startup_no_storage_permission) } } + + if (!userCanGrantWriteExternalStorage()) { + AndroidPermanentlyRevokedPermissionsDialog.show(requireActivity() as AnkiActivity) + } } + + // On SDK 33 (TIRAMISU), `WRITE_EXTERNAL_STORAGE` cannot be set after AnkiDroid 2.15 + // https://github.com/ankidroid/Anki-Android/issues/14423#issuecomment-1777504376 + private fun userCanGrantWriteExternalStorage() = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU } ```
alexanderadam commented 8 months ago

They either use DocumentFile + requesting access to a folder, which is insufficient for database access, or they use MediaStore, which is also insufficient for database access

Would you mind elaborating why this is insufficient?

Usually apps allow syncing and/or exporting for keeping data over installations. Can't Anki do the same?

Syncing seem to be supported already so I understand once there's an import/export possible, the deprecated permission isn't needed Anyway. Or am I missing something here?

What exactly is the advantage of this permission that makes it necessary. Which feature wouldn't work any more once the permission is removed? What exactly is working in Anki that wouldn't work anyway?

david-allison commented 8 months ago

A DocumentFile is an abstraction which can either be a file on the local system, or from a file provider such as Google Drive.

On the local file system, this can be between 20 and 100x slower for basic operations. Listing files can be significantly slower than this (if it works at all). These files do not support random access and can't be passed into any of the standard File-based APIs

On the network, the situation is worse. It depends on external providers, and there are known bugs where folder structures are not represented properly and the listing of files is incomplete

https://commonsware.com/blog/2019/12/01/scoped-storage-stories-problems-saf.html

In my opinion, it is not a safe API which we can use to copy an AnkiDroid collection & media. Especially as a failed transfer would permanently delete user data.


On the Play Store, we only support the removed storage permission if a user is using an older version of Android or if a user has upgraded from 2.15 to 2.16. A fresh install from the Play Store does not use any storage permissions on a modern version of Android.

This issue occurs because a user did not migrate their storage to the new location when prompted, and Android has revoked their permission to the legacy location.

When AnkiDroid had access to the legacy location, it produced backups which can be restored from, BUT: these backups do not include media (we have a huge cohort of users with gigabytes of media)

alexanderadam commented 8 months ago

Thank you so much for your explanations!

In my opinion, it is not a safe API which we can use to copy an AnkiDroid collection & media. Especially as a failed transfer would permanently delete user data.

So how come that users don't experience any deleted data with other apps? For instance apps like Round Sync do heavy I/O and I never experienced any deleted data.

And what is the long-term plan now? Keeping AnkiDroid builds completely unusable instead of risking to use the newer API?

david-allison commented 8 months ago

Round Sync uses MANAGE_EXTERNAL_STORAGE, which we can't use on Google Play:

https://github.com/newhinton/Round-Sync/blob/master/app/src/main/AndroidManifest.xml


The long term plan is above: https://github.com/ankidroid/Anki-Android/issues/14423#issuecomment-1963007547

mikehardy commented 8 months ago

I think that solution is fine in general (https://github.com/ankidroid/Anki-Android/issues/14423#issuecomment-1963007547)

We are already nagging people about doing the migration and syncing, I'm not sure what more we can do?

The string in your proposed patch could perhaps be less specific about no longer able to access after uninstall and more about "Your collection is in the shared storage area of your device, but we no longer have access to it and Google no longer permits us to request access to it." ...then the other stuff. It could be the system revoked permission, it could be they uninstalled/reinstalled, it could be the user themselves revoked permission... we're not sure why but we don't have permission so the string might be genericized a bit to reflect that

david-allison commented 8 months ago

My patch definitely needs some TLC, I won't have time to bring it through

alexanderadam commented 8 months ago

Round Sync uses MANAGE_EXTERNAL_STORAGE, which we can't use on Google Play:

https://github.com/newhinton/Round-Sync/blob/master/app/src/main/AndroidManifest.xml

I think it's also capable of using SAF. It can't do random seek on large file (300 MB) in this example but I don't know whether this is relevant to Anki.

The long term plan is above: #14423 (comment)

Got it :+1:

btw I really appreciate your communication to clarify things. :pray:

TheAdityaGupta commented 8 months ago

May I know what is the current status of this write external storage issue? What is done? What Can't be done? What needs to be done? etc.