Skip to content
Merged
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
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import com.ichi2.anki.common.preferences.sharedPrefs
import com.ichi2.anki.common.storage.AnkiDroidFolder
import com.ichi2.anki.common.storage.CollectionHelper
import com.ichi2.anki.common.storage.StorageDecision
import com.ichi2.anki.common.storage.isLegacyStorage
import com.ichi2.anki.common.utils.android.SdCard
import com.ichi2.anki.compat.CompatHelper.Companion.sdkVersion
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.ui.windows.permissions.InternetPermissionFragment
import com.ichi2.anki.ui.windows.permissions.NotificationsPermissionFragment
import com.ichi2.anki.ui.windows.permissions.PermissionsFragment
Expand Down
9 changes: 2 additions & 7 deletions AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ import androidx.core.content.IntentCompat
import androidx.work.WorkManager
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.common.coroutines.applicationScope
import com.ichi2.anki.common.permissions.hasLegacyStorageAccessPermission
import com.ichi2.anki.common.permissions.isExternalStorageManagerCompat
import com.ichi2.anki.common.preferences.sharedPrefs
import com.ichi2.anki.common.storage.CollectionHelper
import com.ichi2.anki.common.storage.StorageDecision
import com.ichi2.anki.common.storage.grantedStoragePermissions
import com.ichi2.anki.common.utils.android.showThemedToast
import com.ichi2.anki.common.utils.trimToLength
import com.ichi2.anki.dialogs.DialogHandler.Companion.storeMessage
Expand All @@ -29,7 +28,6 @@ import com.ichi2.anki.dialogs.requireDeckPickerOrShowError
import com.ichi2.anki.exception.SystemStorageException
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import com.ichi2.anki.servicelayer.ScopedStorageService
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.ui.windows.reviewer.ReviewerFragment
import com.ichi2.anki.utils.MimeTypeUtils
Expand Down Expand Up @@ -371,10 +369,7 @@ class IntentHandler : AbstractIntentHandler() {
context: Context,
showToast: Boolean,
): Boolean {
val granted =
!ScopedStorageService.isLegacyStorage(context) ||
hasLegacyStorageAccessPermission(context) ||
isExternalStorageManagerCompat()
val granted = grantedStoragePermissions(context)

if (!granted && showToast) {
showThemedToast(context, context.getString(R.string.intent_handler_failed_no_storage_permission), false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,93 +20,11 @@ package com.ichi2.anki.servicelayer
import android.content.Context
import android.os.Build
import android.os.Environment
import com.ichi2.anki.common.preferences.sharedPrefs
import com.ichi2.anki.common.storage.CollectionHelper
import com.ichi2.anki.ui.windows.managespace.isInsideDirectoriesRemovedWithTheApp
import com.ichi2.utils.Permissions
import timber.log.Timber
import java.io.File

object ScopedStorageService {
/**
* Checks if current directory being used by AnkiDroid to store user data is a Legacy Storage Directory.
* This directory is stored under [CollectionHelper.PREF_COLLECTION_PATH] in SharedPreferences
*
* DEPRECATED. Use either [com.ichi2.anki.services.getMediaMigrationState], or
* [com.ichi2.anki.ui.windows.managespace.isInsideDirectoriesRemovedWithTheApp].
*
* @return `true` if AnkiDroid is storing user data in a Legacy Storage Directory.
*
* @throws com.ichi2.anki.exception.SystemStorageException if `getExternalFilesDir` returns null
*/
fun isLegacyStorage(context: Context): Boolean = isLegacyStorage(CollectionHelper.getCurrentAnkiDroidDirectory(context), context)

/**
* Checks if current directory being used by AnkiDroid to store user data is a Legacy Storage Directory.
* This directory is stored under [CollectionHelper.PREF_COLLECTION_PATH] in SharedPreferences
* @return `true` if AnkiDroid is storing user data in a Legacy Storage Directory.
*
* @param setCollectionPath if `false`, null is returned. This stops an infinite loop
* if `isLegacyStorage` is called when obtaining the collection path
*/
fun isLegacyStorage(
context: Context,
setCollectionPath: Boolean,
): Boolean? {
if (!setCollectionPath &&
!context
.sharedPrefs()
.contains(CollectionHelper.PREF_COLLECTION_PATH)
) {
return null
}
return isLegacyStorage(CollectionHelper.getCurrentAnkiDroidDirectory(context), context)
}

/**
* @return `true` if [currentDirPath] is a Legacy Storage Directory.
*
* DEPRECATED. Use [com.ichi2.anki.ui.windows.managespace.isInsideDirectoriesRemovedWithTheApp].
*
*/
fun isLegacyStorage(
currentDirPath: File,
context: Context,
): Boolean {
val internalScopedDirPath =
CollectionHelper.getAppSpecificInternalAnkiDroidDirectory(context)
val currentDir = currentDirPath.canonicalFile
val externalScopedDirs =
CollectionHelper.getAppSpecificExternalDirectories(context).map { it.canonicalFile }
val internalScopedDir = File(internalScopedDirPath).canonicalFile
Timber.i(
"isLegacyStorage(): current dir: %s\nscoped external dirs: %s\nscoped internal dir: %s",
currentDirPath,
externalScopedDirs.joinToString(", "),
internalScopedDirPath,
)

// Loop to check if the current AnkiDroid directory or any of its parents are the same as the root directories
// for app-private external or internal storage - the only directories which will be accessible without
// permissions under scoped storage
val scopedDirectories = externalScopedDirs + internalScopedDir
var currentDirParent: File? = currentDir
while (currentDirParent != null) {
for (scopedDir in scopedDirectories) {
if (currentDirParent.compareTo(scopedDir) == 0) {
Timber.i("isLegacyStorage(): false")
return false
}
}
currentDirParent = currentDirParent.parentFile?.canonicalFile
}

// If the current AnkiDroid directory isn't a sub directory of the app-private external or internal storage
// directories, then it must be in a legacy storage directory
Timber.i("isLegacyStorage(): true")
return true
}

/**
* Whether the user's current collection is now inaccessible due to a 'reinstall'
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private suspend fun Context.getUserDataAndCacheSizeUsingGetPackageSizeInfo(): Lo
*
* externalDirs: /storage/emulated/0/Android/data/com.ichi2.anki
*
* There is a similar method [com.ichi2.anki.servicelayer.ScopedStorageService.isLegacyStorage],
* There is a similar method [com.ichi2.anki.common.storage.isLegacyStorage],
* but it has a different purpose and behavior, and I am not sure if I understand it well.
*/
@Suppress("DEPRECATION") // context.externalMediaDirs: see the doc for the method
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import androidx.annotation.CallSuper
import com.ichi2.anki.IntentHandler.Companion.grantedStoragePermissions
import com.ichi2.anki.common.analytics.Analytics
import com.ichi2.anki.common.analytics.UsageAnalytics
import com.ichi2.anki.common.storage.grantedStoragePermissions
import timber.log.Timber

/**
Expand Down Expand Up @@ -87,7 +87,7 @@ abstract class AnalyticsWidgetProvider : AppWidgetProvider() {
appWidgetIds: IntArray,
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
if (runCatching { grantedStoragePermissions(context, showToast = false) }.getOrNull() != true) {
if (runCatching { grantedStoragePermissions(context) }.getOrNull() != true) {
Timber.w("Opening widget ${this.javaClass.name} without storage access")
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ package com.ichi2.widget
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import com.ichi2.anki.IntentHandler
import com.ichi2.anki.common.android.AnkiBroadcastReceiver
import com.ichi2.anki.common.storage.grantedStoragePermissions

/**
* BroadcastReceiver to handle the scenario where storage permissions are granted,
Expand All @@ -32,7 +32,7 @@ class WidgetPermissionReceiver : AnkiBroadcastReceiver() {
context: Context,
intent: Intent,
) {
if (IntentHandler.grantedStoragePermissions(context, showToast = false)) {
if (grantedStoragePermissions(context)) {
val appWidgetManager = getAppWidgetManager(context) ?: return
val widgetIds = appWidgetManager.getAppWidgetIdsEx(ComponentName(context, AddNoteWidget::class.java))
AddNoteWidget.updateWidgets(context, appWidgetManager, widgetIds)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright (c) 2022 David Allison <davidallisongithub@gmail.com>
// SPDX-FileCopyrightText: Copyright (c) 2022 Arthur Milchior <arthur@milchior.fr>

package com.ichi2.anki.common.storage

import android.content.Context
import com.ichi2.anki.common.permissions.hasLegacyStorageAccessPermission
import com.ichi2.anki.common.permissions.isExternalStorageManagerCompat
import com.ichi2.anki.common.preferences.sharedPrefs
import timber.log.Timber
import java.io.File

/**
* Checks if current directory being used by AnkiDroid to store user data is a Legacy Storage Directory.
* This directory is stored under [CollectionHelper.PREF_COLLECTION_PATH] in SharedPreferences
*
* DEPRECATED. Use either `getMediaMigrationState`, or `isInsideDirectoriesRemovedWithTheApp`.
*
* @return `true` if AnkiDroid is storing user data in a Legacy Storage Directory.
*
* @throws com.ichi2.anki.exception.SystemStorageException if `getExternalFilesDir` returns null
*/
fun isLegacyStorage(context: Context): Boolean = isLegacyStorage(CollectionHelper.getCurrentAnkiDroidDirectory(context), context)

/**
* Checks if current directory being used by AnkiDroid to store user data is a Legacy Storage Directory.
* This directory is stored under [CollectionHelper.PREF_COLLECTION_PATH] in SharedPreferences
* @return `true` if AnkiDroid is storing user data in a Legacy Storage Directory.
*
* @param setCollectionPath if `false`, null is returned. This stops an infinite loop
* if `isLegacyStorage` is called when obtaining the collection path
*/
fun isLegacyStorage(
context: Context,
setCollectionPath: Boolean,
): Boolean? {
if (!setCollectionPath &&
!context
.sharedPrefs()
.contains(CollectionHelper.PREF_COLLECTION_PATH)
) {
return null
}
return isLegacyStorage(CollectionHelper.getCurrentAnkiDroidDirectory(context), context)
}

/**
* @return `true` if [currentDirPath] is a Legacy Storage Directory.
*
* DEPRECATED. Use `isInsideDirectoriesRemovedWithTheApp`.
*
*/
fun isLegacyStorage(
currentDirPath: File,
context: Context,
): Boolean {
val internalScopedDirPath =
CollectionHelper.getAppSpecificInternalAnkiDroidDirectory(context)
val currentDir = currentDirPath.canonicalFile
val externalScopedDirs =
CollectionHelper.getAppSpecificExternalDirectories(context).map { it.canonicalFile }
val internalScopedDir = File(internalScopedDirPath).canonicalFile
Timber.i(
"isLegacyStorage(): current dir: %s\nscoped external dirs: %s\nscoped internal dir: %s",
currentDirPath,
externalScopedDirs.joinToString(", "),
internalScopedDirPath,
)

// Loop to check if the current AnkiDroid directory or any of its parents are the same as the root directories
// for app-private external or internal storage - the only directories which will be accessible without
// permissions under scoped storage
val scopedDirectories = externalScopedDirs + internalScopedDir
var currentDirParent: File? = currentDir
while (currentDirParent != null) {
for (scopedDir in scopedDirectories) {
if (currentDirParent.compareTo(scopedDir) == 0) {
Timber.i("isLegacyStorage(): false")
return false
}
}
currentDirParent = currentDirParent.parentFile?.canonicalFile
}

// If the current AnkiDroid directory isn't a sub directory of the app-private external or internal storage
// directories, then it must be in a legacy storage directory
Timber.i("isLegacyStorage(): true")
return true
}

/** Checks whether storage permissions are granted on the device. If the device is not using legacy storage,
* it verifies if the app has been granted the necessary storage access permission.
* @return `true`: if granted, otherwise `false`
*
* @throws com.ichi2.anki.exception.SystemStorageException if `getExternalFilesDir` returns null
*/
fun grantedStoragePermissions(context: Context): Boolean =
!isLegacyStorage(context) ||
hasLegacyStorageAccessPermission(context) ||
isExternalStorageManagerCompat()
Loading