Skip to content
Open
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
24 changes: 23 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,15 @@ import com.ichi2.anki.dialogs.FatalErrorDialog
import com.ichi2.anki.dialogs.ImportFileSelectionFragment.ApkgImportResultLauncherProvider
import com.ichi2.anki.dialogs.ImportFileSelectionFragment.CsvImportResultLauncherProvider
import com.ichi2.anki.dialogs.ImportViewModel
import com.ichi2.anki.dialogs.ProfileLoadFailureDialog
import com.ichi2.anki.dialogs.SchedulerUpgradeDialog
import com.ichi2.anki.dialogs.SyncErrorDialog
import com.ichi2.anki.dialogs.SyncErrorDialog.Companion.newInstance
import com.ichi2.anki.dialogs.SyncErrorDialog.SyncErrorDialogListener
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.CustomStudyAction
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.CustomStudyAction.Companion.REQUEST_KEY
import com.ichi2.anki.dialogs.registerProfileLoadFailureClosedHandler
import com.ichi2.anki.dialogs.setDeckPickerContextMenuResultListener
import com.ichi2.anki.export.ExportDialogFragment
import com.ichi2.anki.filtered.FilteredDeckOptionsFragment
Expand All @@ -170,6 +172,7 @@ import com.ichi2.anki.introduction.hasCollectionStoragePermissions
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.libanki.sched.DeckNode
import com.ichi2.anki.mediacheck.MediaCheckFragment
import com.ichi2.anki.multiprofile.ProfileManager
import com.ichi2.anki.observability.ChangeManager
import com.ichi2.anki.pages.AnkiPackageImporterFragment
import com.ichi2.anki.progress.observeProgress
Expand Down Expand Up @@ -671,6 +674,11 @@ open class DeckPicker :
openScheduleReminders(did)
}

// A fatal startup error waits until the profile explanation is closed, so it can't cover it
registerProfileLoadFailureClosedHandler {
Comment thread
david-allison marked this conversation as resolved.
Resolved
(viewModel.flowOfStartupResponse.value as? StartupResponse.FatalError)?.let { handleStartupFailure(it.failure) }
}

pullToSyncWrapper.configureView(
this,
IMPORT_MIME_TYPES,
Expand Down Expand Up @@ -961,6 +969,7 @@ open class DeckPicker :
viewModel.flowOfStartupResponse.value = null
invalidateOptionsMenu()
showStartupScreensAndDialogs(sharedPrefs(), 0)
showProfileLoadFailureIfAny()

if (tryShowStudyOptionsPanel()) {
ResizablePaneManager(
Expand All @@ -980,7 +989,11 @@ open class DeckPicker :
// built by the time a failure arrives; rebuild it so onCreateOptionsMenu
// sees the error and blanks it
invalidateOptionsMenu()
handleStartupFailure(response.failure)
if (showProfileLoadFailureIfAny()) {
Timber.i("Showing the startup failure once the profile load failure is closed")
} else {
handleStartupFailure(response.failure)
}
}
}
}
Expand Down Expand Up @@ -1917,6 +1930,15 @@ open class DeckPicker :
}
}

private fun showProfileLoadFailureIfAny(): Boolean {
val failure = ProfileManager.consumeLoadFailure()
if (supportFragmentManager.findFragmentByTag(ProfileLoadFailureDialog.TAG) != null) return true
if (failure == null) return false
Timber.i("Showing the profile load failure")
ProfileLoadFailureDialog.newInstance(failure).show(supportFragmentManager, ProfileLoadFailureDialog.TAG)
return true
}

private fun showCollectionErrorDialog() {
dialogHandler.sendMessage(CollectionLoadingErrorDialog().toMessage())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 Ashish Yadav <mailtoashish693@gmail.com>

package com.ichi2.anki.dialogs

import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.ichi2.anki.CommonString
import com.ichi2.anki.analytics.AnalyticsDialogFragment
import com.ichi2.anki.common.utils.android.showThemedToast
import com.ichi2.anki.multiprofile.ProfileManager
import com.ichi2.anki.multiprofile.ProfileName
import com.ichi2.anki.utils.ext.requireString
import com.ichi2.anki.utils.ext.setFragmentResultListener
import com.ichi2.utils.create
import com.ichi2.utils.message
import com.ichi2.utils.neutralButton
import com.ichi2.utils.positiveButton
import com.ichi2.utils.title
import timber.log.Timber

/** Tells the user their profile couldn't be opened, and offers to open Default from the next start. */
class ProfileLoadFailureDialog : AnalyticsDialogFragment() {
private val profileName get() = ProfileName.fromTrustedSource(requireArguments().requireString(ARG_PROFILE_NAME))
private val defaultProfileName get() = ProfileName.fromTrustedSource(requireArguments().requireString(ARG_DEFAULT_PROFILE_NAME))

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (ProfileManager.LoadFailure(profileName, defaultProfileName) != ProfileManager.loadFailure) {
Timber.w("Dismissing a restored profile load failure that no longer applies")
dismiss()
}
}

override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
AlertDialog.Builder(requireContext()).create {
title(text = getString(CommonString.profile_load_failed_title, profileName.value))
message(text = getString(CommonString.profile_load_failed_message, defaultProfileName.value, profileName.value))
positiveButton(CommonString.dialog_ok) {
Timber.i("Profile load failure: OK pressed")
parentFragmentManager.setProfileLoadFailureClosedResult()
}
neutralButton(text = getString(CommonString.profile_load_failed_use_default, defaultProfileName.value))
}

override fun onStart() {
super.onStart()
(requireDialog() as AlertDialog).neutralButton?.setOnClickListener { useDefaultProfile() }
}

override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
Timber.i("Profile load failure: cancelled")
parentFragmentManager.setProfileLoadFailureClosedResult()
}

private fun useDefaultProfile() {
Timber.i("Profile load failure: Use Default pressed")
if (!ProfileManager.selectDefaultProfile(requireContext().applicationContext)) {
Timber.w("Could not save Default as the profile to open next")
showThemedToast(requireContext(), getString(CommonString.something_wrong), shortLength = true)
return
}
val manager = parentFragmentManager
dismiss()
manager.setProfileLoadFailureClosedResult()
}

companion object {
const val TAG = "profileLoadFailure"
const val REQUEST_PROFILE_LOAD_FAILURE_CLOSED = "request_profile_load_failure_closed"
private const val ARG_PROFILE_NAME = "profileName"
private const val ARG_DEFAULT_PROFILE_NAME = "defaultProfileName"

fun newInstance(failure: ProfileManager.LoadFailure) =
ProfileLoadFailureDialog().apply {
arguments =
Bundle().apply {
putString(ARG_PROFILE_NAME, failure.profileName.value)
putString(ARG_DEFAULT_PROFILE_NAME, failure.defaultProfileName.value)
}
}
}
}

fun FragmentActivity.registerProfileLoadFailureClosedHandler(action: () -> Unit) {
setFragmentResultListener(ProfileLoadFailureDialog.REQUEST_PROFILE_LOAD_FAILURE_CLOSED) { _, _ -> action() }
}

private fun FragmentManager.setProfileLoadFailureClosedResult() {
setFragmentResult(ProfileLoadFailureDialog.REQUEST_PROFILE_LOAD_FAILURE_CLOSED, Bundle())
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

package com.ichi2.anki.multiprofile

import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.webkit.CookieManager
import android.webkit.WebView
import androidx.annotation.CheckResult
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat
import androidx.core.content.edit
Expand Down Expand Up @@ -704,6 +706,11 @@ class ProfileManager private constructor(
*/
object ProfileSwitchContext

data class LoadFailure(
val profileName: ProfileName,
val defaultProfileName: ProfileName,
)

companion object {
private const val MAX_ATTEMPTS = 10
const val PROFILE_REGISTRY_FILENAME = "profiles_prefs"
Expand Down Expand Up @@ -749,6 +756,22 @@ class ProfileManager private constructor(
var attachError: Throwable? = null
private set

var loadFailure: LoadFailure? = null
private set

private var unreportedLoadFailure: LoadFailure? = null

fun consumeLoadFailure(): LoadFailure? = unreportedLoadFailure.also { unreportedLoadFailure = null }

@CheckResult
@SuppressLint("UseKtx") // edit {} drops the commit result, which tells the dialog whether it can close
fun selectDefaultProfile(context: Context): Boolean =
context
.getSharedPreferences(PROFILE_REGISTRY_FILENAME, Context.MODE_PRIVATE)
.edit()
.putString(PREF_LAST_ACTIVE_PROFILE_ID, ProfileId.DEFAULT.value)
.commit()

/**
* Loads the active profile, or returns null when its environment cannot be
* loaded.
Expand All @@ -761,16 +784,37 @@ class ProfileManager private constructor(
fun createOrNull(base: Context): ProfileManager? =
try {
attachError = null
loadFailure = null
unreportedLoadFailure = null
create(base)
} catch (e: Exception) {
// TODO: show this to the user instead of silently falling back to the Default profile
attachError = e
loadFailure = readLoadFailure(base)
unreportedLoadFailure = loadFailure
null
}

private fun readLoadFailure(base: Context): LoadFailure? {
try {
val registryPrefs = (base.applicationContext ?: base).getSharedPreferences(PROFILE_REGISTRY_FILENAME, Context.MODE_PRIVATE)
val registry = ProfileRegistry(registryPrefs)
val failedId = registry.getLastActiveProfileId()?.takeUnless { it.isDefault() } ?: return null
return LoadFailure(
profileName = registry.getProfileMetadata(failedId)?.displayName ?: ProfileName.fromTrustedSource(failedId.value),
defaultProfileName =
registry.getProfileMetadata(ProfileId.DEFAULT)?.displayName
?: ProfileName.fromTrustedSource(DEFAULT_PROFILE_DISPLAY_NAME),
)
} catch (e: Exception) {
return null
}
}

@VisibleForTesting
fun resetForTesting() {
attachError = null
loadFailure = null
unreportedLoadFailure = null
}

/**
Expand Down
Loading
Loading