0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-20 12:02:16 +02:00

Fix UI being in bad state when storage migration fails (#13774)

When migration fails, we used to be stuck in a kind of a limbo:
the migration is considered done (failed, but done) by the service,
but `userMigrationIsInProgress` still returned true, so the UI was not
updated to reflect the state of the migration.

To overcome the issue, this introduces the state of “migration is
paused due to an error”, which is saved in shared preferences.
In this state, the toolbar shows the same migrate button that offers
to resume migration, and the sync icon is hidden.

A few TODOs were also added.

Co-authored-by: Mike Hardy <github@mikehardy.net>
This commit is contained in:
oakkitten 2023-05-13 19:57:51 +01:00 committed by GitHub
parent 330e585985
commit b268a933e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 208 additions and 122 deletions

View File

@ -47,6 +47,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
@ -89,10 +90,12 @@ import com.ichi2.anki.preferences.AdvancedSettingsFragment
import com.ichi2.anki.receiver.SdCardReceiver
import com.ichi2.anki.servicelayer.*
import com.ichi2.anki.servicelayer.SchedulerService.NextCard
import com.ichi2.anki.servicelayer.ScopedStorageService.collectionWillBeMadeInaccessibleAfterUninstall
import com.ichi2.anki.servicelayer.ScopedStorageService.isLegacyStorage
import com.ichi2.anki.servicelayer.ScopedStorageService.userMigrationIsInProgress
import com.ichi2.anki.services.MediaMigrationState
import com.ichi2.anki.services.MigrationService
import com.ichi2.anki.services.PREF_MIGRATION_ERROR_TEXT
import com.ichi2.anki.services.getMediaMigrationState
import com.ichi2.anki.services.withBoundTo
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.stats.AnkiStatsTaskHandler
@ -118,6 +121,7 @@ import com.ichi2.utils.NetworkUtils.isActiveNetworkMetered
import com.ichi2.utils.Permissions.hasStorageAccessPermission
import com.ichi2.widget.WidgetStatus
import kotlinx.coroutines.*
import makeLinksClickable
import net.ankiweb.rsdroid.BackendFactory
import net.ankiweb.rsdroid.RustCleanup
import org.json.JSONException
@ -438,29 +442,36 @@ open class DeckPicker :
*
* See: #5304
* @return true: Interrupt startup. `false`: continue as normal
*
* TODO BEFORE-RELEASE This always returns false.
* Investigate why and either fix the method or make it return Unit.
*/
open fun startingStorageMigrationInterruptsStartup(): Boolean {
val migrationStatus = ScopedStorageService.migrationStatus(this)
Timber.i("migration status: %s", migrationStatus)
when (migrationStatus) {
ScopedStorageService.Status.NEEDS_MIGRATION -> {
// TODO: we should propose a migration, but not yet (alpha users should opt in)
// If the migration was proposed too soon, don't show it again and startup normally.
// TODO: This logic needs thought
// showDialogThatOffersToMigrateStorage(onPostpone = {
// // Unblocks the UI if opened from changing the deck path
// updateDeckList()
// invalidateOptionsMenu()
// handleStartup(skipStorageMigration = true)
// })
return false // TODO: Allow startup normally
val mediaMigrationState = getMediaMigrationState()
Timber.i("migration status: %s", mediaMigrationState)
when (mediaMigrationState) {
is MediaMigrationState.NotOngoing.Needed -> {
// TODO BEFORE-RELEASE we should propose a migration, but not yet (alpha users should opt in)
// If the migration was proposed too soon, don't show it again and startup normally.
// TODO BEFORE-RELEASE This logic needs thought
// showDialogThatOffersToMigrateStorage(onPostpone = {
// // Unblocks the UI if opened from changing the deck path
// updateDeckList()
// invalidateOptionsMenu()
// handleStartup(skipStorageMigration = true)
// })
return false // TODO BEFORE-RELEASE Allow startup normally
}
ScopedStorageService.Status.IN_PROGRESS -> {
is MediaMigrationState.Ongoing.PausedDueToError -> {
showDialogThatOffersToResumeMigrationAfterError(mediaMigrationState.errorText)
return false
}
is MediaMigrationState.Ongoing.NotPaused -> {
startMigrateUserDataService()
return false
}
// App is already using Scoped Storage Directory for user data, no need to migrate & can proceed with startup
ScopedStorageService.Status.COMPLETED, ScopedStorageService.Status.NOT_NEEDED -> return false
is MediaMigrationState.NotOngoing.NotNeeded -> return false
}
}
@ -611,9 +622,9 @@ open class DeckPicker :
* relying instead on modifying it directly and/or using [onPrepareOptionsMenu].
* Note an issue with the latter: https://github.com/ankidroid/Anki-Android/issues/7755
*/
private fun setupMigrationProgressMenuItem(menu: Menu, migrationInProgress: Boolean) {
private fun setupMigrationProgressMenuItem(menu: Menu, mediaMigrationState: MediaMigrationState) {
val migrationProgressMenuItem = menu.findItem(R.id.action_migration_progress)
.apply { isVisible = migrationInProgress }
.apply { isVisible = mediaMigrationState is MediaMigrationState.Ongoing.NotPaused }
suspend fun CircularProgressIndicator.publishProgress(progress: MigrationService.Progress) {
when (progress) {
@ -634,7 +645,7 @@ open class DeckPicker :
}
}
if (migrationInProgress) {
if (mediaMigrationState is MediaMigrationState.Ongoing.NotPaused) {
if (cachedMigrationProgressMenuItemActionView == null) {
val actionView = migrationProgressMenuItem.actionView!!
.also { cachedMigrationProgressMenuItemActionView = it }
@ -708,9 +719,9 @@ open class DeckPicker :
optionsMenuState?.run {
menu.findItem(R.id.deck_picker_action_filter).isVisible = searchIcon
updateUndoIconFromState(menu.findItem(R.id.action_undo), undoIcon)
updateSyncIconFromState(menu.findItem(R.id.action_sync), syncIcon)
menu.findItem(R.id.action_scoped_storage_migrate).isVisible = offerToMigrate
setupMigrationProgressMenuItem(menu, migrationInProgress)
updateSyncIconFromState(menu.findItem(R.id.action_sync), this)
menu.findItem(R.id.action_scoped_storage_migrate).isVisible = shouldShowStartMigrationButton
setupMigrationProgressMenuItem(menu, mediaMigrationState)
}
}
@ -725,34 +736,35 @@ open class DeckPicker :
}
}
private fun updateSyncIconFromState(menuItem: MenuItem, syncIcon: SyncIconState) {
menuItem.setTitle(
when (syncIcon) {
SyncIconState.Normal -> R.string.button_sync
SyncIconState.PendingChanges -> R.string.button_sync
SyncIconState.FullSync -> R.string.sync_menu_title_full_sync
SyncIconState.NotLoggedIn -> R.string.sync_menu_title_no_account
SyncIconState.Disabled -> R.string.button_sync_disabled
}
)
when (syncIcon) {
SyncIconState.Normal -> {
BadgeDrawableBuilder.removeBadge(menuItem)
}
SyncIconState.PendingChanges -> {
BadgeDrawableBuilder(this)
.withColor(ContextCompat.getColor(this@DeckPicker, R.color.badge_warning))
.replaceBadge(menuItem)
}
SyncIconState.FullSync, SyncIconState.NotLoggedIn -> {
BadgeDrawableBuilder(this)
.withText('!')
.withColor(ContextCompat.getColor(this@DeckPicker, R.color.badge_error))
.replaceBadge(menuItem)
}
SyncIconState.Disabled -> {
BadgeDrawableBuilder.removeBadge(menuItem)
menuItem.isVisible = false
private fun updateSyncIconFromState(menuItem: MenuItem, state: OptionsMenuState) {
if (state.mediaMigrationState is MediaMigrationState.Ongoing) {
menuItem.isVisible = false
} else {
menuItem.isVisible = true
menuItem.setTitle(
when (state.syncIcon) {
SyncIconState.Normal, SyncIconState.PendingChanges -> R.string.button_sync
SyncIconState.FullSync -> R.string.sync_menu_title_full_sync
SyncIconState.NotLoggedIn -> R.string.sync_menu_title_no_account
}
)
when (state.syncIcon) {
SyncIconState.Normal -> {
BadgeDrawableBuilder.removeBadge(menuItem)
}
SyncIconState.PendingChanges -> {
BadgeDrawableBuilder(this)
.withColor(ContextCompat.getColor(this@DeckPicker, R.color.badge_warning))
.replaceBadge(menuItem)
}
SyncIconState.FullSync, SyncIconState.NotLoggedIn -> {
BadgeDrawableBuilder(this)
.withText('!')
.withColor(ContextCompat.getColor(this@DeckPicker, R.color.badge_error))
.replaceBadge(menuItem)
}
}
}
}
@ -763,32 +775,28 @@ open class DeckPicker :
val searchIcon = decks.count() >= 10
val undoIcon = undoName(resources).ifEmpty { null }
val syncIcon = fetchSyncStatus(col)
val offerToUpgrade = shouldOfferToUpgrade(context)
val migrationInProgress = userMigrationIsInProgress(context)
OptionsMenuState(searchIcon, undoIcon, syncIcon, offerToUpgrade, migrationInProgress)
val mediaMigrationState = getMediaMigrationState()
val shouldShowStartMigrationButton = shouldOfferToMigrate() ||
mediaMigrationState is MediaMigrationState.Ongoing.PausedDueToError
OptionsMenuState(searchIcon, undoIcon, syncIcon, shouldShowStartMigrationButton, mediaMigrationState)
}
}
private fun shouldOfferToUpgrade(context: Context): Boolean {
// TODO BEFORE-RELEASE This doesn't offer to migrate data if not logged in.
// This should be changed so that we offer to migrate regardless.
private fun shouldOfferToMigrate(): Boolean {
// ALLOW_UNSAFE_MIGRATION skips ensuring that the user is backed up to AnkiWeb
if (!BuildConfig.ALLOW_UNSAFE_MIGRATION && !isLoggedIn()) {
return false
}
return !userMigrationIsInProgress(context) && collectionWillBeMadeInaccessibleAfterUninstall(context)
return getMediaMigrationState() is MediaMigrationState.NotOngoing.Needed
}
private fun fetchSyncStatus(col: Collection): SyncIconState {
val auth = syncAuth()
return when (SyncStatus.getSyncStatus(col, this, auth)) {
SyncStatus.BADGE_DISABLED, SyncStatus.NO_CHANGES -> {
SyncIconState.Normal
}
SyncStatus.ONGOING_MIGRATION -> {
SyncIconState.Disabled
}
SyncStatus.HAS_CHANGES -> {
SyncIconState.PendingChanges
}
SyncStatus.BADGE_DISABLED, SyncStatus.NO_CHANGES -> SyncIconState.Normal
SyncStatus.HAS_CHANGES -> SyncIconState.PendingChanges
SyncStatus.NO_ACCOUNT -> SyncIconState.NotLoggedIn
SyncStatus.FULL_SYNC -> SyncIconState.FullSync
}
@ -817,7 +825,12 @@ open class DeckPicker :
}
R.id.action_scoped_storage_migrate -> {
Timber.i("DeckPicker:: migrate button pressed")
showDialogThatOffersToMigrateStorage(shownAutomatically = false)
val migrationState = getMediaMigrationState()
if (migrationState is MediaMigrationState.Ongoing.PausedDueToError) {
showDialogThatOffersToResumeMigrationAfterError(migrationState.errorText)
} else {
showDialogThatOffersToMigrateStorage(shownAutomatically = false)
}
return true
}
R.id.action_import -> {
@ -1098,7 +1111,7 @@ open class DeckPicker :
launchCatchingTask {
val shownBackupDialog = BackupPromptDialog.showIfAvailable(this@DeckPicker)
if (!shownBackupDialog && shouldOfferToUpgrade(this@DeckPicker) && timeToShowStorageMigrationDialog()) {
if (!shownBackupDialog && shouldOfferToMigrate() && timeToShowStorageMigrationDialog()) {
showDialogThatOffersToMigrateStorage(shownAutomatically = true)
}
}
@ -2593,6 +2606,38 @@ open class DeckPicker :
dialog.addScopedStorageLearnMoreLinkAndShow(message)
}
private fun showDialogThatOffersToResumeMigrationAfterError(errorText: String) {
val helpUrl = getString(R.string.link_migration_failed_dialog_learn_more_en)
val message = getString(R.string.migration__resume_after_failed_dialog__message, errorText, helpUrl)
.parseAsHtml()
AlertDialog.Builder(this)
.setTitle(R.string.scoped_storage_title)
.setMessage(message)
.setNegativeButton(R.string.dialog_cancel) { _, _ -> }
.setPositiveButton(R.string.migration__resume_after_failed_dialog__button_positive) { _, _ ->
getSharedPrefs(this@DeckPicker).edit { remove(PREF_MIGRATION_ERROR_TEXT) }
startMigrateUserDataService()
invalidateOptionsMenu()
}
.create()
.makeLinksClickable()
.show()
}
// TODO BEFORE-RELEASE Fix the logic. As I understand, this works the following way,
// which could make a little more sense:
// if (media sync is not disabled,
// and (either we sync media unconditionally or are on a suitable network),
// and (either we are logged in, or unsafe migration is disallowed (the default))):
// set flag migrate-after-media-synced, and
// call sync, which may fail to actually sync or even fail to start syncing
// (in these cases, migration might start unexpectedly after a successful sync);
// else:
// tell the user that migration is disabled in the settings (might not be true)
// and tell them to sync & backup before continuing (which isn't possible),
// and instead of offering them to force sync,
// offer them to migrate regardless of the above.
private fun performMediaSyncBeforeStorageMigration() {
// if we allow an unsafe migration, the 'sync required' dialog shows an unsafe migration confirmation dialog
val showUnsafeSyncDialog = (BuildConfig.ALLOW_UNSAFE_MIGRATION && !isLoggedIn())
@ -2654,20 +2699,15 @@ data class OptionsMenuState(
/** If undo is available, a string describing the action. */
val undoIcon: String?,
val syncIcon: SyncIconState,
val offerToMigrate: Boolean,
val migrationInProgress: Boolean
val shouldShowStartMigrationButton: Boolean,
val mediaMigrationState: MediaMigrationState
)
enum class SyncIconState {
Normal,
PendingChanges,
FullSync,
NotLoggedIn,
/**
* The icon should appear as disabled. Currently only occurs during scoped storage migration.
*/
Disabled
NotLoggedIn
}
class CollectionLoadingErrorDialog : DialogHandlerMessage(

View File

@ -153,6 +153,8 @@ object ScopedStorageService {
* Whether a user data scoped storage migration is taking place
* This refers to the [MigrateUserData] operation of copying media which can take a long time.
*
* DEPRECATED. Use [com.ichi2.anki.services.getMediaMigrationState] instead.
*
* @throws IllegalStateException If either [PREF_MIGRATION_SOURCE] or [PREF_MIGRATION_DESTINATION] is set (but not both)
* It is a logic bug if only one is set
*/
@ -217,6 +219,10 @@ 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.
*/
fun isLegacyStorage(context: Context): Boolean {
@ -240,6 +246,10 @@ object ScopedStorageService {
/**
* @return `true` if [currentDirPath] is a Legacy Storage Directory.
*
* DEPRECATED. Use either [com.ichi2.anki.services.getMediaMigrationState], or
* [com.ichi2.anki.ui.windows.managespace.isInsideDirectoriesRemovedWithTheApp].
*
*/
fun isLegacyStorage(currentDirPath: String, context: Context): Boolean {
val internalScopedDirPath = CollectionHelper.getAppSpecificInternalAnkiDroidDirectory(context)
@ -272,29 +282,6 @@ object ScopedStorageService {
return true
}
fun migrationStatus(context: Context): Status {
if ((!isLegacyStorage(context) && !userMigrationIsInProgress(context))) {
return Status.COMPLETED
}
if (userMigrationIsInProgress(context)) {
return Status.IN_PROGRESS
}
if (!collectionWillBeMadeInaccessibleAfterUninstall(context)) {
return Status.NOT_NEEDED
}
return Status.NEEDS_MIGRATION
}
enum class Status {
NEEDS_MIGRATION,
IN_PROGRESS,
COMPLETED,
NOT_NEEDED
}
/**
* Whether the user's current collection is now inaccessible due to a 'reinstall'
*
@ -332,6 +319,8 @@ object ScopedStorageService {
/**
* Whether the user's current collection will be inaccessible after uninstalling the app
*
* DEPRECATED. Use [com.ichi2.anki.services.getMediaMigrationState] instead.
*
* @return `false` if:
* * The directory will be **removed** on uninstall
* * The user installed with Android 11+, and is more likely to expect this behavior

View File

@ -29,10 +29,9 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.ichi2.anki.*
import com.ichi2.anki.servicelayer.ScopedStorageService
import com.ichi2.anki.servicelayer.ScopedStorageService.PREF_MIGRATION_DESTINATION
import com.ichi2.anki.servicelayer.ScopedStorageService.PREF_MIGRATION_SOURCE
import com.ichi2.anki.servicelayer.ScopedStorageService.isLegacyStorage
import com.ichi2.anki.servicelayer.ScopedStorageService.userMigrationIsInProgress
import com.ichi2.anki.servicelayer.scopedstorage.MigrateEssentialFiles
import com.ichi2.anki.servicelayer.scopedstorage.MoveConflictedFile
import com.ichi2.anki.servicelayer.scopedstorage.migrateuserdata.MigrateUserData
@ -49,6 +48,10 @@ import java.io.File
import kotlin.math.max
import kotlin.properties.ReadOnlyProperty
// Shared preferences key for user-readable text representing migration error.
// If it is set, it means that media migration is ongoing, but currently paused due to an error.
const val PREF_MIGRATION_ERROR_TEXT = "migrationErrorText"
/**
* A foreground service responsible for migrating the collection
* from a public directory to an app-private directory.
@ -106,6 +109,8 @@ class MigrationService : ServiceWithALifecycleScope(), ServiceWithASimpleBinder<
var flowOfProgress: MutableStateFlow<Progress> = MutableStateFlow(Progress.CalculatingTransferSize)
private val preferences get() = AnkiDroidApp.getSharedPrefs(this)
private lateinit var migrateUserDataTask: MigrateUserData
private var serviceHasBeenStarted = false
@ -113,7 +118,9 @@ class MigrationService : ServiceWithALifecycleScope(), ServiceWithASimpleBinder<
// To simplify things by allowing binding to the service at any time,
// make sure the service has the correct progress emitted even if it is not going to be started.
override fun onCreate() {
if (userMigrationHasSucceeded) flowOfProgress.tryEmit(Progress.Success)
if (getMediaMigrationState() is MediaMigrationState.NotOngoing.NotNeeded) {
flowOfProgress.tryEmit(Progress.Success)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -130,8 +137,7 @@ class MigrationService : ServiceWithALifecycleScope(), ServiceWithASimpleBinder<
flowOfProgress.emit(Progress.CalculatingTransferSize)
try {
migrateUserDataTask = MigrateUserData
.createInstance(AnkiDroidApp.getSharedPrefs(this@MigrationService))
migrateUserDataTask = MigrateUserData.createInstance(preferences)
val remainingTransferSize = getRemainingTransferSize(migrateUserDataTask)
val totalBytesToTransfer = getOrSetTotalTransferSize(valueToPersistIfNotCalculated = remainingTransferSize)
@ -153,7 +159,7 @@ class MigrationService : ServiceWithALifecycleScope(), ServiceWithASimpleBinder<
// on *background* thread, and removed here in another *background* thread.
// These are read from other threads, mostly via userMigrationIsInProgress,
// which might be a race condition and lead to subtle bugs.
AnkiDroidApp.getSharedPrefs(this@MigrationService).edit {
preferences.edit {
remove(PREF_MIGRATION_DESTINATION)
remove(PREF_MIGRATION_SOURCE)
remove(TOTAL_BYTES_TO_TRANSFER_KEY)
@ -162,6 +168,11 @@ class MigrationService : ServiceWithALifecycleScope(), ServiceWithASimpleBinder<
flowOfProgress.emit(Progress.Success)
} catch (e: Exception) {
CrashReportService.sendExceptionReport(e, "Storage migration failed")
preferences.edit {
putString(PREF_MIGRATION_ERROR_TEXT, getUserFriendlyErrorText(e).toString())
}
flowOfProgress.emit(Progress.Failure(e))
}
}
@ -302,7 +313,8 @@ private fun Context.makeMigrationProgressNotification(progress: MigrationService
/**
* A delegate for a property that yields:
* * the [MigrationService] if the migration is in progress, and when the owner is started,
* * the [MigrationService] if media migration is currently ongoing and not paused,
* and when the owner is started,
* * or `null` otherwise.
*
* Note: binding to the service happens fast, but not immediately,
@ -314,7 +326,7 @@ fun <O> O.migrationServiceWhileStartedOrNull(): ReadOnlyProperty<Any?, Migration
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
if (userMigrationIsInProgress(this@migrationServiceWhileStartedOrNull)) {
if (getMediaMigrationState() is MediaMigrationState.Ongoing.NotPaused) {
try {
withBoundTo<MigrationService> {
service = it
@ -330,11 +342,57 @@ fun <O> O.migrationServiceWhileStartedOrNull(): ReadOnlyProperty<Any?, Migration
return ReadOnlyProperty { _, _ -> service }
}
/**************************************************************************************************/
/**
* This assumes that the service is only created if the migration is, was, or is going to run,
* that is when it can "succeed" at all.
* This represents the overarching state of media migration as determined by:
* * the build/flavor,
* * the API level,
* * some settings persisted in shared preferences.
*
* See also the logic in [com.ichi2.anki.DeckPicker.shouldOfferToUpgrade]
* This is not determined by permissions or static variables.
*/
private val Context.userMigrationHasSucceeded get() =
!userMigrationIsInProgress(this) && !isLegacyStorage(this)
sealed interface MediaMigrationState {
sealed interface NotOngoing : MediaMigrationState {
sealed interface NotNeeded : NotOngoing {
object CollectionIsInAppPrivateFolder : NotNeeded
object CollectionIsInPublicFolderButWillRemainAccessible : NotNeeded
}
object Needed : NotOngoing
}
sealed interface Ongoing : MediaMigrationState {
object NotPaused : Ongoing
class PausedDueToError(val errorText: String) : Ongoing
}
}
// TODO Consider refactoring ScopedStorageService to remove its methods used here,
// inlining them, and use this method throughout the app for media migration state.
fun Context.getMediaMigrationState(): MediaMigrationState {
val preferences = AnkiDroidApp.getSharedPrefs(this)
fun migrationIsOngoing() = ScopedStorageService.userMigrationIsInProgress(preferences)
fun collectionIsInAppPrivateDirectory() = !ScopedStorageService.isLegacyStorage(this)
fun collectionWillRemainAccessibleAfterReinstall() =
!ScopedStorageService.collectionWillBeMadeInaccessibleAfterUninstall(this)
return if (migrationIsOngoing()) {
val errorText = preferences.getString(PREF_MIGRATION_ERROR_TEXT, null)
when {
errorText.isNullOrBlank() ->
MediaMigrationState.Ongoing.NotPaused
else ->
MediaMigrationState.Ongoing.PausedDueToError(errorText)
}
} else {
when {
collectionIsInAppPrivateDirectory() ->
MediaMigrationState.NotOngoing.NotNeeded.CollectionIsInAppPrivateFolder
collectionWillRemainAccessibleAfterReinstall() ->
MediaMigrationState.NotOngoing.NotNeeded.CollectionIsInPublicFolderButWillRemainAccessible
else ->
MediaMigrationState.NotOngoing.Needed
}
}
}

View File

@ -22,26 +22,18 @@ import anki.sync.SyncAuth
import anki.sync.SyncStatusResponse
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.SyncPreferences
import com.ichi2.anki.servicelayer.ScopedStorageService.userMigrationIsInProgress
import com.ichi2.libanki.Collection
import net.ankiweb.rsdroid.BackendFactory
// TODO Remove BADGE_DISABLED from this enum, it doesn't belong here
enum class SyncStatus {
NO_ACCOUNT, NO_CHANGES, HAS_CHANGES, FULL_SYNC, BADGE_DISABLED,
/**
* Scope storage migration is ongoing. Sync should be disabled.
*/
ONGOING_MIGRATION;
NO_ACCOUNT, NO_CHANGES, HAS_CHANGES, FULL_SYNC, BADGE_DISABLED;
companion object {
private var sPauseCheckingDatabase = false
private var sMarkedInMemory = false
fun getSyncStatus(col: Collection, context: Context, auth: SyncAuth?): SyncStatus {
if (userMigrationIsInProgress(context)) {
return ONGOING_MIGRATION
}
if (isDisabled) {
return BADGE_DISABLED
}

View File

@ -156,7 +156,6 @@
<string name="sd_card_not_mounted">Device storage not mounted</string>
<string name="empty_cloze_warning">Edit this note and add some cloze deletions. (%1$s)</string>
<string name="button_sync" comment="Text of the sync button">Sync</string>
<string name="button_sync_disabled" comment="Text of the sync button when disabled">Sync temporarily disabled</string>
<string name="button_upgrade" comment="Text of the sync button">Migrate to scoped storage</string>
<string name="cancel_sync_confirm">Do you want to cancel the sync?</string>
<string name="continue_sync">Continue sync</string>
@ -278,6 +277,13 @@
<a href="%2$s">Learn more and get help</a>
]]></string>
<string name="migration__resume_after_failed_dialog__message"><![CDATA[
Storage migration is paused after failing with the error: %1$s.
<br><br>
<a href="%2$s">Learn more and get help</a>
]]></string>
<string name="migration__resume_after_failed_dialog__button_positive">Resume migration</string>
<string name="directory_inaccessible_after_uninstall" comment="Dialog title if AnkiDroid can't access the collection once the app is installed">Inaccessible collection</string>
<string name="directory_inaccessible_after_uninstall_summary" comment="the parameter is the path to the AnkiDroid folder. Typically /storage/emulated/0/AnkiDroid">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:</string>
<string name="restore_data_from_ankiweb">Restore from AnkiWeb (recommended)</string>

View File

@ -410,6 +410,7 @@
<string name="start_migration_progress_message">Starting storage migration. You may resume using AnkiDroid shortly.</string>
<string name="migration_part_1_done_resume">You may resume using AnkiDroid.
\nStorage migration will continue in the background.</string>
<!-- JS Addons -->
<string name="not_valid_js_addon">%s is not a valid javascript addon package</string>
<string name="could_not_create_dir">Could not create directory %s</string>