@@ -5,10 +5,7 @@ package com.ichi2.anki
55
66import android.content.Context
77import android.content.SharedPreferences
8- import android.os.Environment
9- import androidx.annotation.CheckResult
108import androidx.annotation.VisibleForTesting
11- import androidx.core.content.edit
129import com.ichi2.anki.CollectionHelper.PREF_COLLECTION_PATH
1310import com.ichi2.anki.CollectionHelper.getCurrentAnkiDroidDirectory
1411import com.ichi2.anki.common.preferences.sharedPrefs
@@ -17,7 +14,7 @@ import com.ichi2.anki.exception.StorageAccessException
1714import com.ichi2.anki.exception.SystemStorageException
1815import com.ichi2.anki.libanki.Collection
1916import com.ichi2.anki.libanki.CollectionFiles
20- import com.ichi2.anki.storage.AnkiDroidFolder
17+ import com.ichi2.anki.startup.getDefaultAnkiDroidDirectory
2118import com.ichi2.anki.storage.StorageDecision
2219import com.ichi2.preferences.getOrSetString
2320import timber.log.Timber
@@ -96,137 +93,6 @@ object CollectionHelper {
9693 false
9794 }
9895
99- /* *
100- * Get the absolute path to a directory that is suitable to be the default starting location
101- * for the AnkiDroid directory.
102- *
103- * Currently, this is a directory named "AnkiDroid" at the top level of the non-app-specific external storage directory.
104- *
105- * When targeting API > 29, AnkiDroid will have to use Scoped Storage on any device of any API level.
106- * Scoped Storage only allows access to App-Specific directories (without permissions).
107- * Hence, AnkiDroid won't be able to access the directory used currently on all devices,
108- * regardless of their API level, once AnkiDroid targets API > 29.
109- * Instead, AnkiDroid will have to use an App-Specific directory to store the AnkiDroid directory.
110- * This applies to the entire AnkiDroid userbase.
111- *
112- * Currently, if `TESTING_SCOPED_STORAGE` is set to `true`, AnkiDroid uses its External
113- * App-Specific directory.
114- *
115- *
116- * External App-Specific Storage is used since the only advantage Internal App-Specific Storage has over External
117- * App-Specific storage is additional security, but AnkiDroid does not store sensitive data. Defaulting to
118- * External Storage preserves the current behavior of the App
119- * (AnkiDroid defaults to External before the Migration To Scoped Storage).
120- *
121- *
122- * TODO: If External Storage isn't emulated, allow users to choose between External & Internal App-Specific Storage
123- * instead of defaulting to External App-Specific Storage. This should be done since using either one may be more
124- * useful for them. If External Storage is emulated, there is no use in providing the option since Internal
125- * Storage can not provide more storage space than External Storage if External Storage is emulated.
126- *
127- * See the detailed explanation on storage locations & their classification below for more details.
128- *
129- * App-Specific storage refers to directories which are meant to store files that are meant to be used by a
130- * particular app. Each app has its own Internal & External App-Specific directory. Under Scoped Storage,
131- * an app can only access its own Internal & External App-Specific directory without needing permissions.
132- *
133- * Storage can be classified as Internal or External Storage.
134- *
135- * Internal Storage: This storage is characterized by the fact that it is always available since it always resides
136- * on the device's own non-removable storage.
137- *
138- *
139- * App-Specific Internal Storage can be accessed by ONLY the app which owns that directory (without any permissions).
140- * It cannot be accessed by any other apps.
141- * It cannot be accessed using the Files app on Android or by connecting a device to a pc via USB.
142- *
143- * External Storage:
144- *
145- * This storage is characterized only by the fact that it is not guaranteed to be available.
146- *
147- * It may be built-in, non-removable storage on the device which is being emulated to function like external storage.
148- * In this case, it doesn't offer more space than Internal Storage.
149- *
150- * Or, it may be removable storage like an SD Card.
151- *
152- * App-Specific External Storage can be accessed by the app it is owned by without any permissions.
153- * It can be accessed by any apps with the WRITE_EXTERNAL_STORAGE permission.
154- * It can also be accessed via the Android Files app or by connecting the device to a PC via USB.
155- *
156- * Note: The Files app can be misleading. On Samsung devices, clicking on Internal Storage it actually shows the
157- * emulated external storage (/storage/emulated/0/ in my case) - this is because from the point of view of the user,
158- * emulated external storage is just more internal storage since it is built into the phone. This is why vendors
159- * like Samsung may refer to external emulated storage as internal storage, even though for developers, they mean
160- * very different things as explained above.
161- *
162- * @param directoryName The leaf folder name to use at the end of the returned path.
163- * Defaults to `"AnkiDroid"` (the historical default-profile folder name).
164- * Callers wanting a profile-specific layout can pass e.g. the profile id.
165- * @return Absolute Path to the default location starting location for the AnkiDroid directory
166- *
167- * @throws SystemStorageException if `getExternalFilesDir` returns null
168- */
169- // TODO Tracked in https://github.com/ankidroid/Anki-Android/issues/5304
170- @CheckResult
171- fun getDefaultAnkiDroidDirectory (
172- context : Context ,
173- directoryName : String = "AnkiDroid ",
174- ): File {
175- val legacyStorage = selectAnkiDroidFolder(context) != AnkiDroidFolder .APP_PRIVATE
176- return if (legacyStorage) {
177- legacyAnkiDroidDirectory(directoryName)
178- } else {
179- File (getAppSpecificExternalAnkiDroidDirectory(context), directoryName)
180- }
181- }
182-
183- /* *
184- * Returns the absolute path to the AnkiDroid directory under the primary/shared external storage directory.
185- * This directory may be in emulated external storage, or can be an SD Card directory.
186- *
187- * @param directoryName The folder name to use at the end of the returned path. Defaults to
188- * `"AnkiDroid"`. Non-default profiles can pass `ProfileId` here to get a
189- * profile-specific layout.
190- * @return Absolute path to the AnkiDroid directory in primary shared/external storage
191- */
192- private fun legacyAnkiDroidDirectory (directoryName : String = "AnkiDroid "): File =
193- File (Environment .getExternalStorageDirectory(), directoryName)
194-
195- /* *
196- * Returns the absolute path to the AnkiDroid directory under the app-specific, primary/shared external storage
197- * directory.
198- *
199- *
200- * This directory may be in emulated external storage, or can be an SD Card directory.
201- * If it is actually external storage, i.e., removable storage like an SD Card, instead of storage
202- * built into the device itself, using this directory over internal storage can be beneficial since
203- * it may be able to store more data.
204- *
205- *
206- * AnkiDroid can access this directory without permissions, even under Scoped Storage
207- * Other apps can access this directory if they have the WRITE_EXTERNAL_STORAGE permission
208- *
209- * @param context Used to get the External App-Specific directory for AnkiDroid
210- * @return Returns the absolute path to the App-Specific External AnkiDroid directory
211- *
212- * @throws SystemStorageException if `getExternalFilesDir` returns null
213- */
214- private fun getAppSpecificExternalAnkiDroidDirectory (context : Context ): String? {
215- val externalFilesDir = context.getExternalFilesDir(null )
216-
217- // This value *may* be null but we strictly require it. This has caused NullPointerException
218- // in previous releases as we dereference. We can't recover but for purposes of triage,
219- // we will now check for null and if so try to log more information about why.
220- if (externalFilesDir == null ) {
221- Timber .e(" Attempting to determine collection path, but no valid external storage?" )
222- throw SystemStorageException .build(
223- errorDetail = " getExternalFilesDir unexpectedly returned null" ,
224- infoUri = " https://github.com/ankidroid/Anki-Android/issues/13207" ,
225- )
226- }
227- return externalFilesDir.absolutePath
228- }
229-
23096 /* *
23197 * @return Returns an array of [File]s reflecting the directories that AnkiDroid can access without storage permissions
23298 * @see android.content.Context.getExternalFilesDirs
@@ -311,21 +177,6 @@ object CollectionHelper {
311177 )
312178 }
313179
314- /* *
315- * Resets the AnkiDroid directory to [directory]
316- * Note: if [android.R.attr.preserveLegacyExternalStorage] is in use
317- * this will represent a change from `/AnkiDroid` to `/Android/data/...`
318- *
319- * @throws SystemStorageException if `getExternalFilesDir` returns null
320- */
321- fun resetAnkiDroidDirectory (
322- context : Context ,
323- directory : File = getDefaultAnkiDroidDirectory(context),
324- ) {
325- Timber .d(" resetting AnkiDroid directory to %s" , directory)
326- context.sharedPrefs().edit { putString(PREF_COLLECTION_PATH , directory.absolutePath) }
327- }
328-
329180 /* * Test-only override for [storageDecision]. @see ankiDroidDirectoryOverride */
330181 @VisibleForTesting
331182 var storageDecisionTestOverride: StorageDecision ? = null
0 commit comments