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
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2025 Alex Odorico <alex.odorico03@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki.dialogs

import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.CollectionHelper.getCurrentAnkiDroidDirectory
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.DeckPicker
import com.ichi2.anki.R
import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.tests.Shared
import com.ichi2.anki.testutil.discardPreliminaryViews
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
import java.io.File

@RunWith(AndroidJUnit4::class)
class DatabaseCorruptDialogTest : InstrumentedTest() {
private lateinit var targetColFile: File
private lateinit var corruptColFile: File

@Before
fun setup() {
val dbDir = getCurrentAnkiDroidDirectory(testContext)
targetColFile =
File(dbDir, "collection.anki2").apply {
mkdirs()
deleteOnExit()
}
corruptColFile = Shared.getTestFile(testContext, "initial_version_2_12_1_corrupt_regular.anki2")

corruptColFile.copyTo(targetColFile, overwrite = true)
Timber.i("Copied %d bytes to target", targetColFile.length())

ActivityScenario.launch(DeckPicker::class.java)
discardPreliminaryViews()
}

@Test
fun testCorruptDialogFlagSet() {
Assert.assertTrue(DatabaseErrorDialog.databaseCorruptFlag)
}

@Test
fun testOpenCollectionFailedDialog() {
onView(withText(R.string.open_collection_failed_title))
.inRoot(isDialog())
.check(matches(isDisplayed()))
}

@Test
fun testCorruptCollectionDialog() {
val corruptMsg =
testContext.getString(
R.string.corrupt_db_message,
testContext.getString(R.string.repair_deck),
)
onView(withText(corruptMsg))
.inRoot(isDialog())
.check(matches(isDisplayed()))
}

@After
fun cleanupCorruptColFile() {
CollectionManager.closeCollectionBlocking()
if (!targetColFile.delete()) {
Timber.e("Cleanup: Failed to delete %s", targetColFile.path)
}
}
}
8 changes: 8 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.database.sqlite.SQLiteDatabaseCorruptException
import android.net.Uri
import android.view.WindowManager
import android.view.WindowManager.BadTokenException
Expand All @@ -40,6 +41,7 @@ import com.ichi2.anki.CrashReportData.HelpAction
import com.ichi2.anki.CrashReportData.HelpAction.AnkiBackendLink
import com.ichi2.anki.CrashReportData.HelpAction.OpenDeckOptions
import com.ichi2.anki.common.annotations.UseContextParameter
import com.ichi2.anki.dialogs.DatabaseErrorDialog
import com.ichi2.anki.exception.StorageAccessException
import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.pages.DeckOptionsDestination
Expand Down Expand Up @@ -204,6 +206,12 @@ suspend fun <T> FragmentActivity.runCatching(
if (callerTrace != null) Timber.e(callerTrace)
showError(exc.localizedMessage!!, exc.toCrashReportData(this))
}
is SQLiteDatabaseCorruptException -> {
Timber.e(exc, errorMessage)
DatabaseErrorDialog.databaseCorruptFlag = true
if (callerTrace != null) Timber.e(callerTrace)
DatabaseErrorDialog.ShowDatabaseErrorDialog.fromMessage(CollectionLoadingErrorDialog().toMessage())
}
else -> {
Timber.e(exc, errorMessage)
if (callerTrace != null) Timber.e(callerTrace)
Expand Down
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ package com.ichi2.anki

import android.content.Context
import android.content.SharedPreferences
import android.database.sqlite.SQLiteDatabaseCorruptException
import android.database.sqlite.SQLiteFullException
import android.os.Build
import android.os.Environment
import android.os.Parcelable
import androidx.annotation.CheckResult
import androidx.annotation.RequiresApi
import androidx.core.content.edit
import com.ichi2.anki.dialogs.DatabaseErrorDialog
import com.ichi2.anki.exception.StorageAccessException
import com.ichi2.anki.servicelayer.PreferenceUpgradeService
import com.ichi2.anki.servicelayer.PreferenceUpgradeService.setPreferencesUpToDate
Expand Down Expand Up @@ -65,6 +67,10 @@ object InitialActivity {
} catch (e: SQLiteFullException) {
Timber.w(e)
StartupFailure.DiskFull
} catch (e: SQLiteDatabaseCorruptException) {
Timber.w(e)
DatabaseErrorDialog.databaseCorruptFlag = true
StartupFailure.DBError(e)
} catch (e: StorageAccessException) {
// Same handling as the fall through, but without the exception report
// These are now handled with a dialog and don't generate actionable reports
Expand Down