Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle database-related errors / import in AnkiActivity #17512

Merged
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
10 changes: 10 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import com.ichi2.anki.android.input.ShortcutGroup
import com.ichi2.anki.android.input.ShortcutGroupProvider
import com.ichi2.anki.android.input.shortcut
import com.ichi2.anki.dialogs.AsyncDialogFragment
import com.ichi2.anki.dialogs.DatabaseErrorDialog
import com.ichi2.anki.dialogs.DatabaseErrorDialog.DatabaseErrorDialogType
import com.ichi2.anki.dialogs.DialogHandler
import com.ichi2.anki.dialogs.SimpleMessageDialog
import com.ichi2.anki.dialogs.SimpleMessageDialog.SimpleMessageDialogListener
Expand Down Expand Up @@ -89,6 +91,8 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener, Shortc
*/
private var broadcastReceiver: BroadcastReceiver? = null

var importColpkgListener: ImportColpkgListener? = null

/** The name of the parent class (example: 'Reviewer') */
private val activityName: String
val dialogHandler = DialogHandler(this)
Expand Down Expand Up @@ -589,6 +593,12 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener, Shortc
}
}

// Show dialogs to deal with database loading issues etc
open fun showDatabaseErrorDialog(errorDialogType: DatabaseErrorDialogType) {
val newFragment: AsyncDialogFragment = DatabaseErrorDialog.newInstance(errorDialogType)
showAsyncDialogFragment(newFragment)
}

/**
* sets [.getSupportActionBar] and returns the action bar
* @return The action bar which was created
Expand Down
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,12 @@ fun showError(context: Context, msg: String, exception: Throwable, crashReport:
} catch (ex: BadTokenException) {
// issue 12718: activity provided by `context` was not running
Timber.w(ex, "unable to display error dialog")
if (crashReport) {
CrashReportService.sendExceptionReport(
exception,
origin = context::class.java.simpleName
)
}
}
}

Expand Down
17 changes: 4 additions & 13 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ import com.ichi2.anki.dialogs.AsyncDialogFragment
import com.ichi2.anki.dialogs.BackupPromptDialog
import com.ichi2.anki.dialogs.ConfirmationDialog
import com.ichi2.anki.dialogs.CreateDeckDialog
import com.ichi2.anki.dialogs.DatabaseErrorDialog
import com.ichi2.anki.dialogs.DatabaseErrorDialog.DatabaseErrorDialogType
import com.ichi2.anki.dialogs.DeckPickerAnalyticsOptInDialog
import com.ichi2.anki.dialogs.DeckPickerBackupNoSpaceLeftDialog
Expand Down Expand Up @@ -321,8 +320,6 @@ open class DeckPicker :
@VisibleForTesting
internal var focusedDeck: DeckId = 0

var importColpkgListener: ImportColpkgListener? = null

private var toolbarSearchItem: MenuItem? = null
private var toolbarSearchView: AccessibleSearchView? = null
private lateinit var customStudyDialogFactory: CustomStudyDialogFactory
Expand Down Expand Up @@ -1708,12 +1705,6 @@ open class DeckPicker :
}
}

// Show dialogs to deal with database loading issues etc
open fun showDatabaseErrorDialog(errorDialogType: DatabaseErrorDialogType) {
val newFragment: AsyncDialogFragment = DatabaseErrorDialog.newInstance(errorDialogType)
showAsyncDialogFragment(newFragment)
}

override fun showMediaCheckDialog(dialogType: Int) {
showAsyncDialogFragment(MediaCheckDialog.newInstance(dialogType))
}
Expand Down Expand Up @@ -2602,9 +2593,9 @@ class CollectionLoadingErrorDialog : DialogHandlerMessage(
WhichDialogHandler.MSG_SHOW_COLLECTION_LOADING_ERROR_DIALOG,
"CollectionLoadErrorDialog"
) {
override fun handleAsyncMessage(deckPicker: DeckPicker) {
override fun handleAsyncMessage(activity: AnkiActivity) {
// Collection could not be opened
deckPicker.showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_LOAD_FAILED)
activity.showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_LOAD_FAILED)
}

override fun toMessage() = emptyMessage(this.what)
Expand All @@ -2614,7 +2605,7 @@ class OneWaySyncDialog(val message: String?) : DialogHandlerMessage(
which = WhichDialogHandler.MSG_SHOW_ONE_WAY_SYNC_DIALOG,
analyticName = "OneWaySyncDialog"
) {
override fun handleAsyncMessage(deckPicker: DeckPicker) {
override fun handleAsyncMessage(activity: AnkiActivity) {
// Confirmation dialog for one-way sync
val dialog = ConfirmationDialog()
val confirm = Runnable {
Expand All @@ -2623,7 +2614,7 @@ class OneWaySyncDialog(val message: String?) : DialogHandlerMessage(
}
dialog.setConfirm(confirm)
dialog.setArgs(message)
deckPicker.showDialogFragment(dialog)
activity.showDialogFragment(dialog)
}

override fun toMessage(): Message = Message.obtain().apply {
Expand Down
20 changes: 10 additions & 10 deletions AnkiDroid/src/main/java/com/ichi2/anki/Import.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ import timber.log.Timber
// onSelectedPackageToImport/onSelectedCsvForImport
// importUtils - copying selected file into local cache
// ImportDialog - confirmation screen after file copied to cache
// * ImportDialogListener - DeckPicker implementation of handler for the confirmation screen
// * DeckPicker.importAdd/importReplace - called from confirmation screen
// * ImportDialogListener - AnkiActivity implementation of handler for the confirmation screen
// * AnkiActivity.importAdd/importReplace - called from confirmation screen
// BackendBackups/BackendImporting - new backend for importing
// importReplaceListener - old backend listener for importing

Expand All @@ -46,7 +46,7 @@ fun interface ImportColpkgListener {
}

@NeedsTest("successful import from the app menu")
fun DeckPicker.onSelectedPackageToImport(data: Intent) {
fun AnkiActivity.onSelectedPackageToImport(data: Intent) {
val importResult = ImportUtils.handleFileImport(this, data)
if (!importResult.isSuccess) {
runOnUiThread {
Expand All @@ -71,12 +71,12 @@ fun Activity.onSelectedCsvForImport(data: Intent) {
stackBuilder.startActivities()
}

fun DeckPicker.showImportDialog(id: Int, importPath: String) {
fun AnkiActivity.showImportDialog(id: Int, importPath: String) {
Timber.d("showImportDialog() delegating to ImportDialog")
val newFragment: AsyncDialogFragment = ImportDialog.newInstance(id, importPath)
showAsyncDialogFragment(newFragment)
}
fun DeckPicker.showImportDialog() {
fun AnkiActivity.showImportDialog() {
showImportDialog(
ImportOptions(
importApkg = true,
Expand All @@ -86,18 +86,18 @@ fun DeckPicker.showImportDialog() {
)
}

fun DeckPicker.showImportDialog(options: ImportOptions) {
fun AnkiActivity.showImportDialog(options: ImportOptions) {
showDialogFragment(ImportFileSelectionFragment.newInstance(options))
}

class DatabaseRestorationListener(val deckPicker: DeckPicker, val newAnkiDroidDirectory: String) : ImportColpkgListener {
class DatabaseRestorationListener(val activity: AnkiActivity, val newAnkiDroidDirectory: String) : ImportColpkgListener {
override fun onImportColpkg(colpkgPath: String?) {
Timber.i("Database restoration correct")
deckPicker.sharedPrefs().edit {
activity.sharedPrefs().edit {
putString("deckPath", newAnkiDroidDirectory)
}
deckPicker.dismissAllDialogFragments()
deckPicker.importColpkgListener = null
activity.dismissAllDialogFragments()
activity.importColpkgListener = null
CollectionHelper.ankiDroidDirectoryOverride = null
}
}
15 changes: 14 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,20 @@ class IntentHandler : AbstractIntentHandler() {
which = WhichDialogHandler.MSG_DO_SYNC,
analyticName = "DoSyncDialog"
) {
override fun handleAsyncMessage(deckPicker: DeckPicker) {
override fun handleAsyncMessage(activity: AnkiActivity) {
// we may be called via any AnkiActivity but sync is a DeckPicker thing
if (activity !is DeckPicker) {
showError(
activity,
activity.getString(R.string.something_wrong),
ClassCastException(activity.javaClass.simpleName + " is not " + DeckPicker.javaClass.simpleName),
true
)
return
}
// let's be clear about the type now that we've checked
val deckPicker = activity

val preferences = deckPicker.sharedPrefs()
val res = deckPicker.resources
val hkey = preferences.getString("hkey", "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ class DatabaseErrorDialog : AsyncDialogFragment() {
try {
sqliteInstalled = Runtime.getRuntime().exec("sqlite3 --version").waitFor() == 0
} catch (e: IOException) {
Timber.w(e)
Timber.i("sqlite3 not installed: ${e.message}")
} catch (e: InterruptedException) {
Timber.w(e)
Timber.i("test for sqlite3 failed: ${e.message}")
}
return when (requireDialogType()) {
DIALOG_LOAD_FAILED -> {
Expand Down Expand Up @@ -408,16 +408,15 @@ class DatabaseErrorDialog : AsyncDialogFragment() {
R.string.restore_data_from_backup,
dismissesDialog = false,
{ activity ->
val deckPicker = activity as DeckPicker
Timber.i("Restoring from colpkg")
val newAnkiDroidDirectory = CollectionHelper.getDefaultAnkiDroidDirectory(deckPicker)
deckPicker.importColpkgListener = DatabaseRestorationListener(deckPicker, newAnkiDroidDirectory)
val newAnkiDroidDirectory = CollectionHelper.getDefaultAnkiDroidDirectory(activity)
activity.importColpkgListener = DatabaseRestorationListener(activity, newAnkiDroidDirectory)

deckPicker.launchCatchingTask {
activity.launchCatchingTask {
CollectionHelper.ankiDroidDirectoryOverride = newAnkiDroidDirectory

CollectionManager.withCol {
deckPicker.showImportDialog(
activity.showImportDialog(
ImportOptions(
importTextFile = false,
importColpkg = true,
Expand Down Expand Up @@ -555,7 +554,7 @@ class DatabaseErrorDialog : AsyncDialogFragment() {
private fun requireDialogType() = BundleCompat.getParcelable(requireArguments(), "dialog", DatabaseErrorDialogType::class.java)!!

fun dismissAllDialogFragments() {
(activity as DeckPicker).dismissAllDialogFragments()
(activity as AnkiActivity).dismissAllDialogFragments()
}

@Parcelize
Expand Down Expand Up @@ -608,8 +607,8 @@ class DatabaseErrorDialog : AsyncDialogFragment() {
which = WhichDialogHandler.MSG_SHOW_DATABASE_ERROR_DIALOG,
analyticName = "DatabaseErrorDialog"
) {
override fun handleAsyncMessage(deckPicker: DeckPicker) {
deckPicker.showDatabaseErrorDialog(dialogType)
override fun handleAsyncMessage(activity: AnkiActivity) {
activity.showDatabaseErrorDialog(dialogType)
}

override fun toMessage(): Message = Message.obtain().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ class DialogHandler(activity: AnkiActivity) : Handler(getDefaultLooper()) {
val msg = DialogHandlerMessage.fromMessage(message)
UsageAnalytics.sendAnalyticsScreenView(msg.analyticName)
Timber.i("Handling Message: %s", msg.analyticName)
val deckPicker = activity.get() as DeckPicker
msg.handleAsyncMessage(deckPicker)
msg.handleAsyncMessage(activity.get() as AnkiActivity)
}

/**
Expand Down Expand Up @@ -100,7 +99,7 @@ class DialogHandler(activity: AnkiActivity) : Handler(getDefaultLooper()) {
*/
abstract class DialogHandlerMessage protected constructor(val which: WhichDialogHandler, val analyticName: String) {
val what = which.what
abstract fun handleAsyncMessage(deckPicker: DeckPicker)
abstract fun handleAsyncMessage(activity: AnkiActivity)

protected fun emptyMessage(what: Int): Message = Message.obtain().apply { this.what = what }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import android.os.Bundle
import android.os.Message
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import com.ichi2.anki.AnkiActivity
import com.ichi2.anki.DeckPicker
import com.ichi2.anki.R
import com.ichi2.anki.showError
import com.ichi2.utils.negativeButton
import com.ichi2.utils.positiveButton

Expand Down Expand Up @@ -64,9 +66,19 @@ class ExportReadyDialog(private val listener: ExportReadyDialogListener) : Async
which = WhichDialogHandler.MSG_EXPORT_READY,
analyticName = "ExportReadyDialog"
) {
override fun handleAsyncMessage(deckPicker: DeckPicker) {
deckPicker.showDialogFragment(
deckPicker.exportingDelegate.dialogsFactory.newExportReadyDialog().withArguments(exportPath)
override fun handleAsyncMessage(activity: AnkiActivity) {
// we may be called via any AnkiActivity but export is a DeckPicker thing
if (activity !is DeckPicker) {
showError(
activity,
activity.getString(R.string.something_wrong),
ClassCastException(activity.javaClass.simpleName + " is not " + DeckPicker.javaClass.simpleName),
true
)
return
}
activity.showDialogFragment(
activity.exportingDelegate.dialogsFactory.newExportReadyDialog().withArguments(exportPath)
)
}

Expand Down
16 changes: 14 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/dialogs/MediaCheckDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import androidx.annotation.CheckResult
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import com.ichi2.anki.AnkiActivity
import com.ichi2.anki.DeckPicker
import com.ichi2.anki.R
import com.ichi2.anki.showError
import com.ichi2.libanki.MediaCheckResult

class MediaCheckDialog : AsyncDialogFragment() {
Expand Down Expand Up @@ -174,12 +176,22 @@ class MediaCheckDialog : AsyncDialogFragment() {
private val unused: ArrayList<String>?,
private val invalid: ArrayList<String>?
) : DialogHandlerMessage(WhichDialogHandler.MSG_SHOW_MEDIA_CHECK_COMPLETE_DIALOG, "MediaCheckCompleteDialog") {
override fun handleAsyncMessage(deckPicker: DeckPicker) {
override fun handleAsyncMessage(activity: AnkiActivity) {
// Media check results
val id = dialogType
if (id != DIALOG_CONFIRM_MEDIA_CHECK) {
// we may be called via any AnkiActivity but media check is a DeckPicker thing
if (activity !is DeckPicker) {
showError(
activity,
activity.getString(R.string.something_wrong),
ClassCastException(activity.javaClass.simpleName + " is not " + DeckPicker.javaClass.simpleName),
true
)
return
}
val checkList = MediaCheckResult(noHave ?: arrayListOf(), unused ?: arrayListOf(), invalid ?: arrayListOf())
deckPicker.showMediaCheckDialog(id, checkList)
activity.showMediaCheckDialog(id, checkList)
}
}

Expand Down
15 changes: 13 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/dialogs/SyncErrorDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.ichi2.anki.ConflictResolution
import com.ichi2.anki.DeckPicker
import com.ichi2.anki.R
import com.ichi2.anki.joinSyncMessages
import com.ichi2.anki.showError

class SyncErrorDialog : AsyncDialogFragment() {
interface SyncErrorDialogListener {
Expand Down Expand Up @@ -277,8 +278,18 @@ class SyncErrorDialog : AsyncDialogFragment() {
private val dialogType: Int,
private val dialogMessage: String?
) : DialogHandlerMessage(WhichDialogHandler.MSG_SHOW_SYNC_ERROR_DIALOG, "SyncErrorDialog") {
override fun handleAsyncMessage(deckPicker: DeckPicker) {
deckPicker.showSyncErrorDialog(dialogType, dialogMessage)
override fun handleAsyncMessage(activity: AnkiActivity) {
// we may be called via any AnkiActivity but media check is a DeckPicker thing
if (activity !is DeckPicker) {
showError(
activity,
activity.getString(R.string.something_wrong),
ClassCastException(activity.javaClass.simpleName + " is not " + DeckPicker.javaClass.simpleName),
true
)
return
}
activity.showSyncErrorDialog(dialogType, dialogMessage)
}

override fun toMessage(): Message = Message.obtain().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ package com.ichi2.anki.preferences

import android.content.Context
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.BuildConfig
import com.ichi2.anki.CollectionHelper
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.CrashReportService
import com.ichi2.anki.R
Expand Down Expand Up @@ -86,6 +89,24 @@ class DevOptionsFragment : SettingsFragment() {
launchCatchingTask { CollectionManager.withCol { Thread.sleep(1000 * 86400) } }
false
}
// Make it possible to test crash reporting
requirePreference<Preference>(R.string.pref_set_database_path_debug_key).setOnPreferenceClickListener {
AlertDialog.Builder(requireContext()).show {
setTitle("Warning!")
setMessage("This will most likely make it so that you cannot access your collection. It will be very difficult to recover your data.")
setPositiveButton(R.string.dialog_ok) { _, _ ->
Timber.w("Setting collection path to /storage/emulated/0/AnkiDroid")
AnkiDroidApp.sharedPrefs().edit {
putString(
CollectionHelper.PREF_COLLECTION_PATH,
"/storage/emulated/0/AnkiDroid"
)
}
}
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
david-allison marked this conversation as resolved.
Show resolved Hide resolved
}
false
}

val sizePreference = requirePreference<IncrementerNumberRangePreferenceCompat>(getString(R.string.pref_fill_collection_size_file_key))
val numberOfFilePreference = requirePreference<IncrementerNumberRangePreferenceCompat>(getString(R.string.pref_fill_collection_number_file_key))
Expand Down
Loading