Skip to content

Commit 1be4ad3

Browse files
david-allisonmikehardy
authored andcommitted
docs: introduce and document SystemStorageException
This bug is commonly triggered by an `Android/data` directory corruption bug on Pixel phones: https://issuetracker.google.com/issues/460912704 But there are other unknown causes It should be handled on startup Issue 13207
1 parent dd3fa30 commit 1be4ad3

File tree

6 files changed

+82
-3
lines changed

6 files changed

+82
-3
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import com.ichi2.anki.dialogs.ExportReadyDialog.Companion.KEY_EXPORT_PATH
7575
import com.ichi2.anki.dialogs.ExportReadyDialog.Companion.REQUEST_EXPORT_SAVE
7676
import com.ichi2.anki.dialogs.ExportReadyDialog.Companion.REQUEST_EXPORT_SHARE
7777
import com.ichi2.anki.dialogs.SimpleMessageDialog
78+
import com.ichi2.anki.exception.SystemStorageException
7879
import com.ichi2.anki.libanki.Collection
7980
import com.ichi2.anki.preferences.sharedPrefs
8081
import com.ichi2.anki.receiver.SdCardReceiver
@@ -740,6 +741,8 @@ open class AnkiActivity(
740741
*
741742
* @return `true`: activity may continue to start, `false`: [onCreate] should stop executing
742743
* as storage permissions are mot granted
744+
*
745+
* @throws SystemStorageException if `getExternalFilesDir` returns null
743746
*/
744747
fun ensureStoragePermissions(): Boolean {
745748
if (IntentHandler.grantedStoragePermissions(this, showToast = true)) {

AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.ichi2.anki.CollectionHelper.getCurrentAnkiDroidDirectory
2828
import com.ichi2.anki.CollectionHelper.getDefaultAnkiDroidDirectory
2929
import com.ichi2.anki.backend.createDatabaseUsingAndroidFramework
3030
import com.ichi2.anki.exception.StorageAccessException
31+
import com.ichi2.anki.exception.SystemStorageException
3132
import com.ichi2.anki.exception.UnknownDatabaseVersionException
3233
import com.ichi2.anki.libanki.Collection
3334
import com.ichi2.anki.libanki.CollectionFiles
@@ -175,6 +176,8 @@ object CollectionHelper {
175176
* very different things as explained above.
176177
*
177178
* @return Absolute Path to the default location starting location for the AnkiDroid directory
179+
*
180+
* @throws SystemStorageException if `getExternalFilesDir` returns null
178181
*/
179182
// TODO Tracked in https://github.com/ankidroid/Anki-Android/issues/5304
180183
@CheckResult
@@ -215,6 +218,8 @@ object CollectionHelper {
215218
*
216219
* @param context Used to get the External App-Specific directory for AnkiDroid
217220
* @return Returns the absolute path to the App-Specific External AnkiDroid directory
221+
*
222+
* @throws SystemStorageException if `getExternalFilesDir` returns null
218223
*/
219224
private fun getAppSpecificExternalAnkiDroidDirectory(context: Context): String? {
220225
val externalFilesDir = context.getExternalFilesDir(null)
@@ -224,9 +229,9 @@ object CollectionHelper {
224229
// we will now check for null and if so try to log more information about why.
225230
if (externalFilesDir == null) {
226231
Timber.e("Attempting to determine collection path, but no valid external storage?")
227-
throw IllegalStateException(
228-
"getExternalFilesDir unexpectedly returned null. Media state: " +
229-
Environment.getExternalStorageState(),
232+
throw SystemStorageException.build(
233+
errorDetail = "getExternalFilesDir unexpectedly returned null",
234+
infoUri = "https://github.com/ankidroid/Anki-Android/issues/13207",
230235
)
231236
}
232237
return externalFilesDir.absolutePath
@@ -262,6 +267,8 @@ object CollectionHelper {
262267

263268
/**
264269
* @return the absolute path to the AnkiDroid directory.
270+
*
271+
* @throws SystemStorageException if `getExternalFilesDir` returns null
265272
*/
266273
fun getCurrentAnkiDroidDirectory(context: Context): File =
267274
getCurrentAnkiDroidDirectoryOptionalContext(context.sharedPrefs()) { context }

AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.ichi2.anki.common.utils.trimToLength
3232
import com.ichi2.anki.dialogs.DialogHandler.Companion.storeMessage
3333
import com.ichi2.anki.dialogs.DialogHandlerMessage
3434
import com.ichi2.anki.dialogs.requireDeckPickerOrShowError
35+
import com.ichi2.anki.exception.SystemStorageException
3536
import com.ichi2.anki.libanki.DeckId
3637
import com.ichi2.anki.noteeditor.NoteEditorLauncher
3738
import com.ichi2.anki.servicelayer.ScopedStorageService
@@ -350,6 +351,8 @@ class IntentHandler : AbstractIntentHandler() {
350351
/** Checks whether storage permissions are granted on the device. If the device is not using legacy storage,
351352
* it verifies if the app has been granted the necessary storage access permission.
352353
* @return `true`: if granted, otherwise `false` and shows a missing permission toast
354+
*
355+
* @throws SystemStorageException if `getExternalFilesDir` returns null
353356
*/
354357
fun grantedStoragePermissions(
355358
context: Context,

AnkiDroid/src/main/java/com/ichi2/anki/exception/StorageAccessException.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
package com.ichi2.anki.exception
1818

19+
/**
20+
* If a known storage path is unusable
21+
*
22+
* @see [SystemStorageException] if no path is available
23+
*/
1924
class StorageAccessException(
2025
msg: String? = null,
2126
e: Throwable? = null,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2025 David Allison <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki.exception
18+
19+
import android.os.Environment
20+
import timber.log.Timber
21+
22+
/**
23+
* A failure when Android is unable to provide a storage path
24+
*
25+
* For issue #13207: special handling when `getExternalFilesDir` is `null`
26+
* Commonly triggered by an `Android/data` directory corruption bug on Pixel phones:
27+
* https://issuetracker.google.com/issues/460912704
28+
*
29+
* @see [StorageAccessException] for issues with a known storage path
30+
*/
31+
class SystemStorageException private constructor(
32+
errorDetail: String,
33+
val externalStorageState: String,
34+
val infoUri: String?,
35+
) : IllegalStateException("$errorDetail. Media state: $externalStorageState") {
36+
override val message: String
37+
get() = super.message!!
38+
39+
companion object {
40+
fun build(
41+
errorDetail: String,
42+
infoUri: String? = null,
43+
): SystemStorageException {
44+
val storageState =
45+
try {
46+
Environment.getExternalStorageState()
47+
} catch (e: Exception) {
48+
Timber.w(e, "getExternalStorageState")
49+
"ERROR"
50+
}
51+
52+
return SystemStorageException(
53+
errorDetail = errorDetail,
54+
externalStorageState = storageState,
55+
infoUri = infoUri,
56+
)
57+
}
58+
}
59+
}

AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/ScopedStorageService.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ object ScopedStorageService {
3636
* [com.ichi2.anki.ui.windows.managespace.isInsideDirectoriesRemovedWithTheApp].
3737
*
3838
* @return `true` if AnkiDroid is storing user data in a Legacy Storage Directory.
39+
*
40+
* @throws com.ichi2.anki.exception.SystemStorageException if `getExternalFilesDir` returns null
3941
*/
4042
fun isLegacyStorage(context: Context): Boolean = isLegacyStorage(CollectionHelper.getCurrentAnkiDroidDirectory(context), context)
4143

0 commit comments

Comments
 (0)