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

Collection not opened: Add button to copy stack trace & debug info #17232

Open
david-allison opened this issue Oct 11, 2024 · 14 comments · May be fixed by #17279
Open

Collection not opened: Add button to copy stack trace & debug info #17232

david-allison opened this issue Oct 11, 2024 · 14 comments · May be fixed by #17279

Comments

@david-allison
Copy link
Member

The 'Collection not opened' dialog does not allow a user to see either a stack trace of the error, or copy debug information to ask for help.

An item should be added to the 'Error handling' dialog to allow a user to 'Copy debug info'.

This should copy the following to the clipboard:

  • The stack trace of the error which occurred (if known)
  • Standard info from DebugInfoService

Once the following PR is merged, the patch supplied in the PR description will quickly open the dialog

Important

Getting the exception stack trace into the dialog will be more difficult than expected

Screenshots

Image

Image

@Aditya13s
Copy link
Contributor

I am working on it.

@Aditya13s
Copy link
Contributor

Aditya13s commented Oct 12, 2024

@david-allison How to reproduce this error

@david-allison
Copy link
Member Author

Once the following PR is merged, the patch supplied in the PR description will quickly open the dialog

@Aditya13s
Copy link
Contributor

CopyDebugInfo.mp4

@david-allison Is this the desired output?

@david-allison
Copy link
Member Author

@Aditya13s I'm struggling to read that (in future, could you post the text into a <details>)

But that doesn't look like the BackendFatalException

@Aditya13s
Copy link
Contributor

dalvik.system.VMStack.getThreadStackTrace(Native Method)
java.lang.Thread.getStackTrace(Thread.java:1841)
com.ichi2.anki.dialogs.DatabaseErrorDialog$copyStackTraceAndDebugInfo$1.invokeSuspend(DatabaseErrorDialog.kt:418)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:363)
kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:21)
kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:88)
kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:123)
kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:52)
kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:43)
kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
com.ichi2.anki.dialogs.DatabaseErrorDialog.copyStackTraceAndDebugInfo(DatabaseErrorDialog.kt:417)
com.ichi2.anki.dialogs.DatabaseErrorDialog.onCreateDialog$lambda$10$lambda$9(DatabaseErrorDialog.kt:222)
com.ichi2.anki.dialogs.DatabaseErrorDialog.$r8$lambda$Df9vk-0Vl_5ea4aPRzUpYHh5vj4(Unknown Source:0)
com.ichi2.anki.dialogs.DatabaseErrorDialog$$ExternalSyntheticLambda6.invoke(D8$$SyntheticClass:0)
com.ichi2.utils.AlertDialogFacadeKt.listItems$lambda$13(AlertDialogFacade.kt:335)
com.ichi2.utils.AlertDialogFacadeKt.$r8$lambda$T_yyJNcrPd1vevHz2myNBe-li_s(Unknown Source:0)
com.ichi2.utils.AlertDialogFacadeKt$$ExternalSyntheticLambda3.onClick(D8$$SyntheticClass:0)
androidx.appcompat.app.AlertController$AlertParams$3.onItemClick(AlertController.java:1068)
android.widget.AdapterView.performItemClick(AdapterView.java:352)
android.widget.AbsListView.performItemClick(AbsListView.java:1234)
android.widget.AbsListView$PerformClick.run(AbsListView.java:3231)
android.os.Handler.handleCallback(Handler.java:938)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loopOnce(Looper.java:233)
android.os.Looper.loop(Looper.java:344)
android.app.ActivityThread.main(ActivityThread.java:8249)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:589)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1071)

This is the stack trace which i get using this code :-

    val stackTrace = buildStackTraceString(Thread.currentThread().stackTrace)

    private fun buildStackTraceString(elements: Array<StackTraceElement>?): String {
        val sb = StringBuilder()
        elements?.let {
            for (element in it) {
                sb.append(element.toString() + "\n")
            }
        }
        return sb.toString()
    }

@david-allison
Copy link
Member Author

You want the stack trace of the error which caused the dialog to open, not that

@Aditya13s
Copy link
Contributor

Is this the error which you are talking about

InitialActivity com.ichi2.anki.debug net.ankiweb.rsdroid.BackendException$BackendFatalError:
at com.ichi2.anki.CollectionManager$CollectionOpenFailure.triggerFailure(CollectionManager.kt:420)
at com.ichi2.anki.CollectionManager.ensureOpenInner(CollectionManager.kt:241)
at com.ichi2.anki.CollectionManager.getColUnsafe$lambda$11$lambda$10(CollectionManager.kt:299)
at com.ichi2.anki.CollectionManager.$r8$lambda$1xvLXT2AdUAF2dT9vOLsddKVcWo(Unknown Source:0)
at com.ichi2.anki.CollectionManager$$ExternalSyntheticLambda2.invoke(D8$$SyntheticClass:0)
at com.ichi2.anki.CollectionManager$withQueue$3.invokeSuspend(CollectionManager.kt:103)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)

@david-allison
Copy link
Member Author

Yes

@Aditya13s
Copy link
Contributor

The above stack trace it coming from InitialActivity.kt. To print the error, stack track have to store in a string in InitialActivity and pass it to the DatabaseErrorDialog.kt

@david-allison
Copy link
Member Author

Can't you convert the enum to a discriminated union, and pass along the error as a parameter?

@Aditya13s
Copy link
Contributor

@david-allison
I successfully convert the StartupFailure enum to sealed class in InitialActivity and can easily share the exception happening in InitialActivity with the DeckPicker and use a funtion Log.getStackTrace(Exception) to get the stack trace but now i have challenges with sharing the exception from DeckPicker to DatabaseErrorDialog.

Changes which I made are:-

InitialActivity.kt

In this class I changed StartupFailure enum to sealed class in which i can pass the exception .

sealed class StartupFailure {
         data class SD_CARD_NOT_MOUNTED(val exception: Exception? = null) : StartupFailure()
         data class DIRECTORY_NOT_ACCESSIBLE(val exception: Exception? = null) : StartupFailure()
         data class FUTURE_ANKIDROID_VERSION(val exception: Exception? = null) : StartupFailure()
         data class DB_ERROR(val exception: Exception? = null) : StartupFailure()
         data class DATABASE_LOCKED(val exception: Exception? = null) : StartupFailure()
         data class WEBVIEW_FAILED(val exception: Exception? = null) : StartupFailure()
         data class DISK_FULL(val exception: Exception? = null) : StartupFailure()
    }

fun getStartupFailureType(context: Context): StartupFailure? {
        // A WebView failure means that we skip `AnkiDroidApp`, and therefore haven't loaded the collection
        if (AnkiDroidApp.webViewFailedToLoad()) {
            return StartupFailure.WEBVIEW_FAILED()
        }

        val failure = try {
            CollectionManager.getColUnsafe()
            return null
        } catch (e: BackendException.BackendDbException.BackendDbLockedException) {
            Timber.w(e)
            StartupFailure.DATABASE_LOCKED(e)
        } catch (e: BackendException.BackendDbException.BackendDbFileTooNewException) {
            Timber.w(e)
            StartupFailure.FUTURE_ANKIDROID_VERSION(e)
        } catch (e: SQLiteFullException) {
            Timber.w(e)
            StartupFailure.DISK_FULL(e)
        } catch (e: Exception) {
            Timber.w(e)
            CrashReportService.sendExceptionReport(e, "InitialActivity::getStartupFailureType")
            StartupFailure.DB_ERROR(e)
        }

        if (!AnkiDroidApp.isSdCardMounted) {
            return StartupFailure.SD_CARD_NOT_MOUNTED()
        } else if (!CollectionHelper.isCurrentAnkiDroidDirAccessible(context)) {
            return StartupFailure.DIRECTORY_NOT_ACCESSIBLE()
        }

        return failure
    }

  1. DeckPicker.kt
fun handleStartupFailure(failure: StartupFailure?) {
        when (failure) {
            is SD_CARD_NOT_MOUNTED -> {
                Timber.i("SD card not mounted")
                onSdCardNotMounted()
            }
            is DIRECTORY_NOT_ACCESSIBLE -> {
                Timber.i("AnkiDroid directory inaccessible")
                if (ScopedStorageService.collectionWasMadeInaccessibleAfterUninstall(this)) {
                    showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_STORAGE_UNAVAILABLE_AFTER_UNINSTALL)
                } else {
                    val i = AdvancedSettingsFragment.getSubscreenIntent(this)
                    requestPathUpdateLauncher.launch(i)
                    showThemedToast(this, R.string.directory_inaccessible, false)
                }
            }
            is FUTURE_ANKIDROID_VERSION -> {
                Timber.i("Displaying database versioning")
                showDatabaseErrorDialog(DatabaseErrorDialogType.INCOMPATIBLE_DB_VERSION)
            }
            is DATABASE_LOCKED -> {
                Timber.i("Displaying database locked error")
                showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_DB_LOCKED)
            }
            is WEBVIEW_FAILED -> AlertDialog.Builder(this).show {
                title(R.string.ankidroid_init_failed_webview_title)
                message(
                    text = getString(
                        R.string.ankidroid_init_failed_webview,
                        AnkiDroidApp.webViewErrorMessage
                    )
                )
                positiveButton(R.string.close) {
                    closeCollectionAndFinish()
                }
                cancelable(false)
            }
            is DISK_FULL -> displayNoStorageError()
            is DB_ERROR -> displayDatabaseFailure(failure.exception)
            else -> displayDatabaseFailure()
        }

@david-allison
Copy link
Member Author

Just do it for the one exception for now

@Aditya13s
Copy link
Contributor

@david-allison Finally able to complete the task

CopyDebugInfo.mp4

@Aditya13s Aditya13s linked a pull request Oct 18, 2024 that will close this issue
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants