diff --git a/.editorconfig b/.editorconfig index 1f2d56b0b105..0e99246d32c7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,2 @@ [*.{kt,kts}] -ktlint_disabled_rules = no-wildcard-imports -# ktlint_standard_no-wildcard-imports = disabled \ No newline at end of file +ktlint_standard_no-wildcard-imports = disabled \ No newline at end of file diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt index 5e073594fee6..e171ca6e1e78 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt @@ -66,8 +66,8 @@ class DeckPickerTest : InstrumentedTest() { onView(withId(R.id.files)).perform( RecyclerViewActions.actionOnItem( hasDescendant(withText("TestDeck$testString")), - clickChildViewWithId(R.id.counts_layout) - ) + clickChildViewWithId(R.id.counts_layout), + ), ) // without this sleep, the study options fragment sometimes loses the "load and become active" race vs the assertion below. @@ -77,7 +77,7 @@ class DeckPickerTest : InstrumentedTest() { // Check if currently open Activity is StudyOptionsActivity assertThat( activityInstance, - instanceOf(StudyOptionsActivity::class.java) + instanceOf(StudyOptionsActivity::class.java), ) } @@ -108,8 +108,8 @@ class DeckPickerTest : InstrumentedTest() { onView(withId(R.id.files)).perform( RecyclerViewActions.actionOnItem( hasDescendant(withText("TestDeck$testString")), - clickChildViewWithId(R.id.counts_layout) - ) + clickChildViewWithId(R.id.counts_layout), + ), ) // Create a card belonging to the new deck, using Basic type (guaranteed to exist) diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/FieldEditLineTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/FieldEditLineTest.kt index 0f36b8845588..1942f866e3db 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/FieldEditLineTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/FieldEditLineTest.kt @@ -26,11 +26,12 @@ import java.util.concurrent.atomic.AtomicReference class FieldEditLineTest : NoteEditorTest() { @Test fun testSetters() { - val line = fieldEditLine().apply { - setContent("Hello", true) - name = "Name" - setOrd(5) - } + val line = + fieldEditLine().apply { + setContent("Hello", true) + name = "Name" + setOrd(5) + } val text = line.editText assertThat(text.ord, equalTo(5)) assertThat(text.text.toString(), equalTo("Hello")) @@ -39,11 +40,12 @@ class FieldEditLineTest : NoteEditorTest() { @Test fun testSaveRestore() { - val toSave = fieldEditLine().apply { - setContent("Hello", true) - name = "Name" - setOrd(5) - } + val toSave = + fieldEditLine().apply { + setContent("Hello", true) + name = "Name" + setOrd(5) + } val b = toSave.onSaveInstanceState() val restored = fieldEditLine() diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt index 7188dbac3953..e13947db6331 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt @@ -44,7 +44,7 @@ class NoteEditorTabOrderTest : NoteEditorTest() { java.lang.AssertionError: - Expected: is "a"""" + Expected: is "a"""", ) @Throws(Throwable::class) fun testTabOrder() { @@ -66,7 +66,10 @@ class NoteEditorTabOrderTest : NoteEditorTest() { } } - private fun sendKeyDownUp(activity: Activity, keyCode: Int) { + private fun sendKeyDownUp( + activity: Activity, + keyCode: Int, + ) { val focusedView = activity.currentFocus if (focusedView != null) { val inputConnection = BaseInputConnection(focusedView, true) @@ -78,7 +81,7 @@ class NoteEditorTabOrderTest : NoteEditorTest() { @Throws(Throwable::class) private fun onActivity( scenario: ActivityScenario, - noteEditorActivityAction: ActivityAction + noteEditorActivityAction: ActivityAction, ) { val wrapped = AtomicReference(null) scenario.onActivity { a: NoteEditor -> diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt index 30c4dd12ab02..16c147e878b0 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt @@ -34,9 +34,10 @@ abstract class NoteEditorTest protected constructor() { var runtimePermissionRule: TestRule? = GrantStoragePermission.instance @get:Rule - var activityRule: ActivityScenarioRule? = ActivityScenarioRule( - noteEditorIntent - ) + var activityRule: ActivityScenarioRule? = + ActivityScenarioRule( + noteEditorIntent, + ) private val noteEditorIntent: Intent get() { @@ -53,20 +54,18 @@ abstract class NoteEditorTest protected constructor() { "Test fails on Travis API $invalid", Build.VERSION.SDK_INT, not( - equalTo(invalid) - ) + equalTo(invalid), + ), ) } } private val invalidSdksImpl: List get() { - // TODO: Look into these assumptions and see if they can be diagnosed - both work on my emulators. - // If we fix them, we might be able to use instrumentation.sendKeyDownUpSync /* java.lang.AssertionError: Activity never becomes requested state "[DESTROYED]" (last lifecycle transition = "PAUSED") at androidx.test.core.app.ActivityScenario.waitForActivityToBecomeAnyOf(ActivityScenario.java:301) - */ + */ val invalid = Build.VERSION_CODES.N_MR1 val integers = ArrayList(listOf(invalid)) integers.addAll(invalidSdks!!) diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesActivityTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesActivityTest.kt index 144f48e59281..bfed86788df0 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesActivityTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesActivityTest.kt @@ -68,16 +68,17 @@ class PagesActivityTest : InstrumentedTest() { @JvmStatic // required for initParameters fun initParameters(): Collection> { /** See [PageFragment] */ - val intents = listOf Intent, String>>( - Pair(PagesActivityTest::getStatistics, "Statistics"), - Pair(PagesActivityTest::getCardInfo, "CardInfo"), - Pair(PagesActivityTest::getCongratsPage, "CongratsPage"), - Pair(PagesActivityTest::getDeckOptions, "DeckOptions"), - // the following need a file path - Pair(PagesActivityTest::needsPath, "AnkiPackageImporterFragment"), - Pair(PagesActivityTest::needsPath, "CsvImporter"), - Pair(PagesActivityTest::needsPath, "ImageOcclusion") - ) + val intents = + listOf Intent, String>>( + Pair(PagesActivityTest::getStatistics, "Statistics"), + Pair(PagesActivityTest::getCardInfo, "CardInfo"), + Pair(PagesActivityTest::getCongratsPage, "CongratsPage"), + Pair(PagesActivityTest::getDeckOptions, "DeckOptions"), + // the following need a file path + Pair(PagesActivityTest::needsPath, "AnkiPackageImporterFragment"), + Pair(PagesActivityTest::needsPath, "CsvImporter"), + Pair(PagesActivityTest::needsPath, "ImageOcclusion"), + ) return intents.map { arrayOf(it.first, it.second) } } @@ -101,11 +102,14 @@ fun PagesActivityTest.getCongratsPage(context: Context): Intent { CardInfoDestination(card.id).toIntent(context) } } + fun PagesActivityTest.getDeckOptions(context: Context): Intent { return DeckOptions.getIntent(context, col.decks.allNamesAndIds().first().id) } -fun PagesActivityTest.needsPath(@Suppress("UNUSED_PARAMETER") context: Context): Intent { +fun PagesActivityTest.needsPath( + @Suppress("UNUSED_PARAMETER") context: Context, +): Intent { assumeThat("not implemented: path needed", false, equalTo(true)) TODO() } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt index ff9a309cb43c..8257f3b79b89 100755 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt @@ -1,146 +1,145 @@ -/* - * Copyright (c) 2023 - * - * 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 . - */ -package com.ichi2.anki - -import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.ichi2.anki.tests.InstrumentedTest -import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission -import com.ichi2.anki.testutil.grantPermissions -import com.ichi2.anki.testutil.notificationPermission -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import java.util.concurrent.TimeUnit - -@RunWith(AndroidJUnit4::class) -class ReviewerTest : InstrumentedTest() { - - // Launch IntroductionActivity instead of DeckPicker activity because in CI - // builds, it seems to create IntroductionActivity after the DeckPicker, - // causing the DeckPicker activity to be destroyed. As a consequence, this - // will throw RootViewWithoutFocusException when Espresso tries to interact - // with an already destroyed activity. By launching IntroductionActivity, we - // ensure that IntroductionActivity is launched first and navigate to the - // DeckPicker -> Reviewer activities - @get:Rule - val activityScenarioRule = ActivityScenarioRule(IntroductionActivity::class.java) - - @get:Rule - val runtimePermissionRule = grantPermissions(storagePermission, notificationPermission) - - @Test - fun testCustomSchedulerWithCustomData() { - col.config.set( - "cardStateCustomizer", - """ - states.good.normal.review.easeFactor = 3.0; - states.good.normal.review.scheduledDays = 123; - customData.good.c += 1; - """ - ) - val note = addNoteUsingBasicModel("foo", "bar") - val card = note.firstCard() - val deck = col.decks.get(note.notetype.did)!! - card.moveToReviewQueue() - col.backend.updateCards( - listOf( - card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build() - ), - true - ) - - closeGetStartedScreenIfExists() - closeBackupCollectionDialogIfExists() - reviewDeckWithName(deck.name) - - var cardFromDb = col.getCard(card.id).toBackendCard() - assertThat(cardFromDb.easeFactor, equalTo(card.factor)) - assertThat(cardFromDb.interval, equalTo(card.ivl)) - assertThat(cardFromDb.customData, equalTo("""{"c":1}""")) - - clickShowAnswerAndAnswerGood() - - cardFromDb = col.getCard(card.id).toBackendCard() - assertThat(cardFromDb.easeFactor, equalTo(3000)) - assertThat(cardFromDb.interval, equalTo(123)) - assertThat(cardFromDb.customData, equalTo("""{"c":2}""")) - } - - private fun closeGetStartedScreenIfExists() { - onView(withId(R.id.get_started)).withFailureHandler { _, _ -> }.perform(click()) - } - - private fun closeBackupCollectionDialogIfExists() { - onView(withText(R.string.button_backup_later)) - .withFailureHandler { _, _ -> } - .perform(click()) - } - - private fun clickOnDeckWithName(deckName: String) { - onView(withId(R.id.files)).checkWithTimeout(matches(hasDescendant(withText(deckName)))) - onView(withId(R.id.files)).perform( - RecyclerViewActions.actionOnItem( - hasDescendant(withText(deckName)), - click() - ) - ) - } - - private fun clickOnStudyButtonIfExists() { - onView(withId(R.id.studyoptions_start)) - .withFailureHandler { _, _ -> } - .perform(click()) - } - - private fun reviewDeckWithName(deckName: String) { - clickOnDeckWithName(deckName) - // Adding cards directly to the database while in the Deck Picker screen - // will not update the page with correct card counts. Hence, clicking - // on the deck will bring us to the study options page where we need to - // click on the Study button. If we have added cards to the database - // before the Deck Picker screen has fully loaded, then we skip clicking - // the Study button - clickOnStudyButtonIfExists() - } - - private fun clickShowAnswerAndAnswerGood() { - onView(withId(R.id.flashcard_layout_flip)).perform(click()) - // We need to wait for the card to fully load to allow enough time for - // the messages to be passed in and out of the WebView when evaluating - // the custom JS scheduler code. The ease buttons are hidden until the - // custom scheduler has finished running - onView(withId(R.id.flashcard_layout_ease3)).checkWithTimeout( - matches(isDisplayed()), - 100, - // Increase to a max of 30 seconds because CI builds can be very - // slow - TimeUnit.SECONDS.toMillis(30) - ) - onView(withId(R.id.flashcard_layout_ease3)).perform(click()) - } -} +/* + * Copyright (c) 2023 + * + * 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 . + */ +package com.ichi2.anki + +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ichi2.anki.tests.InstrumentedTest +import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission +import com.ichi2.anki.testutil.grantPermissions +import com.ichi2.anki.testutil.notificationPermission +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class ReviewerTest : InstrumentedTest() { + // Launch IntroductionActivity instead of DeckPicker activity because in CI + // builds, it seems to create IntroductionActivity after the DeckPicker, + // causing the DeckPicker activity to be destroyed. As a consequence, this + // will throw RootViewWithoutFocusException when Espresso tries to interact + // with an already destroyed activity. By launching IntroductionActivity, we + // ensure that IntroductionActivity is launched first and navigate to the + // DeckPicker -> Reviewer activities + @get:Rule + val activityScenarioRule = ActivityScenarioRule(IntroductionActivity::class.java) + + @get:Rule + val runtimePermissionRule = grantPermissions(storagePermission, notificationPermission) + + @Test + fun testCustomSchedulerWithCustomData() { + col.config.set( + "cardStateCustomizer", + """ + states.good.normal.review.easeFactor = 3.0; + states.good.normal.review.scheduledDays = 123; + customData.good.c += 1; + """, + ) + val note = addNoteUsingBasicModel("foo", "bar") + val card = note.firstCard() + val deck = col.decks.get(note.notetype.did)!! + card.moveToReviewQueue() + col.backend.updateCards( + listOf( + card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build(), + ), + true, + ) + + closeGetStartedScreenIfExists() + closeBackupCollectionDialogIfExists() + reviewDeckWithName(deck.name) + + var cardFromDb = col.getCard(card.id).toBackendCard() + assertThat(cardFromDb.easeFactor, equalTo(card.factor)) + assertThat(cardFromDb.interval, equalTo(card.ivl)) + assertThat(cardFromDb.customData, equalTo("""{"c":1}""")) + + clickShowAnswerAndAnswerGood() + + cardFromDb = col.getCard(card.id).toBackendCard() + assertThat(cardFromDb.easeFactor, equalTo(3000)) + assertThat(cardFromDb.interval, equalTo(123)) + assertThat(cardFromDb.customData, equalTo("""{"c":2}""")) + } + + private fun closeGetStartedScreenIfExists() { + onView(withId(R.id.get_started)).withFailureHandler { _, _ -> }.perform(click()) + } + + private fun closeBackupCollectionDialogIfExists() { + onView(withText(R.string.button_backup_later)) + .withFailureHandler { _, _ -> } + .perform(click()) + } + + private fun clickOnDeckWithName(deckName: String) { + onView(withId(R.id.files)).checkWithTimeout(matches(hasDescendant(withText(deckName)))) + onView(withId(R.id.files)).perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText(deckName)), + click(), + ), + ) + } + + private fun clickOnStudyButtonIfExists() { + onView(withId(R.id.studyoptions_start)) + .withFailureHandler { _, _ -> } + .perform(click()) + } + + private fun reviewDeckWithName(deckName: String) { + clickOnDeckWithName(deckName) + // Adding cards directly to the database while in the Deck Picker screen + // will not update the page with correct card counts. Hence, clicking + // on the deck will bring us to the study options page where we need to + // click on the Study button. If we have added cards to the database + // before the Deck Picker screen has fully loaded, then we skip clicking + // the Study button + clickOnStudyButtonIfExists() + } + + private fun clickShowAnswerAndAnswerGood() { + onView(withId(R.id.flashcard_layout_flip)).perform(click()) + // We need to wait for the card to fully load to allow enough time for + // the messages to be passed in and out of the WebView when evaluating + // the custom JS scheduler code. The ease buttons are hidden until the + // custom scheduler has finished running + onView(withId(R.id.flashcard_layout_ease3)).checkWithTimeout( + matches(isDisplayed()), + 100, + // Increase to a max of 30 seconds because CI builds can be very + // slow + TimeUnit.SECONDS.toMillis(30), + ) + onView(withId(R.id.flashcard_layout_ease3)).perform(click()) + } +} diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt index 6650b0c9000d..b42d9ae66473 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt @@ -28,7 +28,6 @@ import org.hamcrest.Matcher import kotlin.math.min object TestUtils { - /** * Get instance of current activity */ @@ -38,7 +37,7 @@ object TestUtils { InstrumentationRegistry.getInstrumentation().runOnMainSync { val resumedActivities: Collection<*> = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage( - Stage.RESUMED + Stage.RESUMED, ) if (resumedActivities.iterator().hasNext()) { val currentActivity = resumedActivities.iterator().next() as Activity @@ -75,7 +74,10 @@ object TestUtils { return "Click on a child view with specified id." } - override fun perform(uiController: UiController, view: View) { + override fun perform( + uiController: UiController, + view: View, + ) { val v = view.findViewById(id) v.performClick() } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/compat/CompatNormalizeTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/compat/CompatNormalizeTest.kt index 8c86b1530509..108e81c619a6 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/compat/CompatNormalizeTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/compat/CompatNormalizeTest.kt @@ -29,7 +29,10 @@ import java.util.Locale class CompatNormalizeTest : InstrumentedTest() { @Test fun normalize() { - fun assertEqual(l: Locale, str: String) { + fun assertEqual( + l: Locale, + str: String, + ) { val normalized = CompatHelper.compat.normalize(l) assertThat(normalized.toLanguageTag(), equalTo(str)) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/ModelEditorContextMenuTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/ModelEditorContextMenuTest.kt index e46afb07583e..201e628156d3 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/ModelEditorContextMenuTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/ModelEditorContextMenuTest.kt @@ -39,7 +39,7 @@ class ModelEditorContextMenuTest : InstrumentedTest() { fun showsAllOptionsIfAboveN() { launchFragment( fragmentArgs = bundleOf(ModelEditorContextMenu.KEY_LABEL to testDialogTitle), - themeResId = R.style.Theme_Light + themeResId = R.style.Theme_Light, ) { MockModelEditorContextMenu(isAtLeastAtN = true) } onView(withText(testDialogTitle)).check(matches(isDisplayed())) ModelEditorContextMenuAction.entries.forEach { @@ -52,12 +52,12 @@ class ModelEditorContextMenuTest : InstrumentedTest() { fun doesNotShowLanguageHintOptionIfBelowN() { launchFragment( fragmentArgs = bundleOf(ModelEditorContextMenu.KEY_LABEL to testDialogTitle), - themeResId = R.style.Theme_Light + themeResId = R.style.Theme_Light, ) { MockModelEditorContextMenu(isAtLeastAtN = false) } onView(withText(testDialogTitle)).check(matches(isDisplayed())) // ModelEditorContextMenuAction.AddLanguageHint shouldn't be available onView(withText(ModelEditorContextMenuAction.AddLanguageHint.actionTextId)).check( - doesNotExist() + doesNotExist(), ) // make sure we aren't losing other items besides ModelEditorContextMenuAction.AddLanguageHint ModelEditorContextMenuAction.entries @@ -67,7 +67,7 @@ class ModelEditorContextMenuTest : InstrumentedTest() { } class MockModelEditorContextMenu( - private val isAtLeastAtN: Boolean + private val isAtLeastAtN: Boolean, ) : ModelEditorContextMenu() { override fun isAtLeastAtN(): Boolean = isAtLeastAtN } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt index dccb1a934ab5..bd6b8309c485 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewer/PeripheralKeymapTest.kt @@ -26,6 +26,7 @@ import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.hasSize import org.junit.Test import org.junit.runner.RunWith + @RunWith(AndroidJUnit4::class) class PeripheralKeymapTest : InstrumentedTest() { @Test @@ -39,16 +40,16 @@ class PeripheralKeymapTest : InstrumentedTest() { peripheralKeymap.onKeyDown( KeyEvent.KEYCODE_NUMPAD_1, - getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1) + getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1), ) peripheralKeymap.onKeyUp( KeyEvent.KEYCODE_NUMPAD_1, - getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1) + getNumpadEvent(KeyEvent.KEYCODE_NUMPAD_1), ) assertThat>(processed, hasSize(1)) assertThat( processed[0], - equalTo(ViewerCommand.FLIP_OR_ANSWER_EASE1) + equalTo(ViewerCommand.FLIP_OR_ANSWER_EASE1), ) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt index 6b0b6184a9ae..6c23f0d9682b 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt @@ -69,7 +69,7 @@ class ACRATest : InstrumentedTest() { assertArrayEquals( "Debug logcat arguments not set correctly", CrashReportService.acraCoreConfigBuilder.build().logcatArguments.toTypedArray(), - mDebugLogcatArguments + mDebugLogcatArguments, ) verifyDebugACRAPreferences() } @@ -78,13 +78,13 @@ class ACRATest : InstrumentedTest() { assertTrue( "ACRA was not disabled correctly", sharedPrefs - .getBoolean(ACRA.PREF_DISABLE_ACRA, true) + .getBoolean(ACRA.PREF_DISABLE_ACRA, true), ) assertEquals( "ACRA feedback was not turned off correctly", CrashReportService.FEEDBACK_REPORT_NEVER, sharedPrefs - .getString(CrashReportService.FEEDBACK_REPORT_KEY, "undefined") + .getString(CrashReportService.FEEDBACK_REPORT_KEY, "undefined"), ) } @@ -130,27 +130,29 @@ class ACRATest : InstrumentedTest() { // The same class/method combo is only sent once, so we face a new method each time (should test that system later) val crash = Exception("testCrashReportSend at " + System.currentTimeMillis()) - val trace = arrayOf( - StackTraceElement( - "Class", - "Method" + System.currentTimeMillis().toInt(), - "File", - System.currentTimeMillis().toInt() + val trace = + arrayOf( + StackTraceElement( + "Class", + "Method" + System.currentTimeMillis().toInt(), + "File", + System.currentTimeMillis().toInt(), + ), ) - ) crash.stackTrace = trace // one send should work - val crashData = CrashReportDataFactory( - testContext, - CrashReportService.acraCoreConfigBuilder.build() - ).createCrashData(ReportBuilder().exception(crash)) + val crashData = + CrashReportDataFactory( + testContext, + CrashReportService.acraCoreConfigBuilder.build(), + ).createCrashData(ReportBuilder().exception(crash)) assertTrue( LimitingReportAdministrator().shouldSendReport( testContext, CrashReportService.acraCoreConfigBuilder.build(), - crashData - ) + crashData, + ), ) // A second send should not work @@ -158,8 +160,8 @@ class ACRATest : InstrumentedTest() { LimitingReportAdministrator().shouldSendReport( testContext, CrashReportService.acraCoreConfigBuilder.build(), - crashData - ) + crashData, + ), ) // Now let's clear data @@ -170,8 +172,8 @@ class ACRATest : InstrumentedTest() { LimitingReportAdministrator().shouldSendReport( testContext, CrashReportService.acraCoreConfigBuilder.build(), - crashData - ) + crashData, + ), ) } @@ -231,7 +233,10 @@ class ACRATest : InstrumentedTest() { } @Throws(ACRAConfigurationException::class) - private fun assertDialogEnabledStatus(message: String, isEnabled: Boolean) { + private fun assertDialogEnabledStatus( + message: String, + isEnabled: Boolean, + ) { val config = CrashReportService.acraCoreConfigBuilder.build() for (configuration in config.pluginConfigurations) { // Make sure the dialog is set to pop up @@ -252,13 +257,15 @@ class ACRATest : InstrumentedTest() { } @Throws(ACRAConfigurationException::class) - private fun assertToastMessage(@StringRes res: Int) { + private fun assertToastMessage( + @StringRes res: Int, + ) { val config = CrashReportService.acraCoreConfigBuilder.build() for (configuration in config.pluginConfigurations) { if (configuration.javaClass.toString().contains("Toast")) { assertEquals( mApp!!.resources.getString(res), - (configuration as ToastConfiguration).text + (configuration as ToastConfiguration).text, ) assertTrue("Toast should be enabled", configuration.enabled()) } @@ -268,7 +275,7 @@ class ACRATest : InstrumentedTest() { private fun verifyACRANotDisabled() { assertFalse( "ACRA was not enabled correctly", - sharedPrefs.getBoolean(ACRA.PREF_DISABLE_ACRA, false) + sharedPrefs.getBoolean(ACRA.PREF_DISABLE_ACRA, false), ) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt index 32edebe879e0..f1d3e0185483 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -76,7 +76,7 @@ class ContentProviderTest : InstrumentedTest() { */ @Before @Throws( - Exception::class + Exception::class, ) @KotlinCleanup("remove 'requireNoNulls' and fix mDummyFields") fun setUp() { @@ -99,7 +99,8 @@ class ContentProviderTest : InstrumentedTest() { /* Looping over all parents of full name. Adding them to * mTestDeckIds ensures the deck parents decks get deleted * too at tear-down. - */for (s in path) { + */ + for (s in path) { partialName += s /* If parent already exists, don't add the deck, so * that we are sure it won't get deleted at @@ -134,7 +135,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that remnant notes have been deleted", 0, - col.findNotes("tag:$TEST_TAG").size + col.findNotes("tag:$TEST_TAG").size, ) } // delete test decks @@ -142,7 +143,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that all created decks have been deleted", mNumDecksBeforeTest, - col.decks.count() + col.decks.count(), ) // Delete test model col.modSchemaNoCheck() @@ -151,7 +152,10 @@ class ContentProviderTest : InstrumentedTest() { } @Throws(Exception::class) - private fun removeAllModelsByName(col: com.ichi2.libanki.Collection, name: String) { + private fun removeAllModelsByName( + col: com.ichi2.libanki.Collection, + name: String, + ) { var testModel = col.notetypes.byName(name) while (testModel != null) { col.notetypes.rem(testModel) @@ -164,19 +168,21 @@ class ContentProviderTest : InstrumentedTest() { // called by android.database.CursorToBulkCursorAdapter // This is called by API clients implicitly, but isn't done by this test class val firstNote = getFirstCardFromScheduler(col) - val noteProjection = arrayOf( - FlashCardsContract.Note._ID, - FlashCardsContract.Note.FLDS, - FlashCardsContract.Note.TAGS - ) + val noteProjection = + arrayOf( + FlashCardsContract.Note._ID, + FlashCardsContract.Note.FLDS, + FlashCardsContract.Note.TAGS, + ) val resolver = contentResolver - val cursor = resolver.query( - FlashCardsContract.Note.CONTENT_URI_V2, - noteProjection, - "id=" + firstNote!!.nid, - null, - null - ) + val cursor = + resolver.query( + FlashCardsContract.Note.CONTENT_URI_V2, + noteProjection, + "id=" + firstNote!!.nid, + null, + null, + ) assertNotNull(cursor) val window = CursorWindow("test") @@ -196,11 +202,12 @@ class ContentProviderTest : InstrumentedTest() { // Get required objects for test val cr = contentResolver // Add the note - val values = ContentValues().apply { - put(FlashCardsContract.Note.MID, mModelId) - put(FlashCardsContract.Note.FLDS, Utils.joinFields(TEST_NOTE_FIELDS)) - put(FlashCardsContract.Note.TAGS, TEST_TAG) - } + val values = + ContentValues().apply { + put(FlashCardsContract.Note.MID, mModelId) + put(FlashCardsContract.Note.FLDS, Utils.joinFields(TEST_NOTE_FIELDS)) + put(FlashCardsContract.Note.TAGS, TEST_TAG) + } val newNoteUri = cr.insert(FlashCardsContract.Note.CONTENT_URI, values) assertNotNull("Check that URI returned from addNewNote is not null", newNoteUri) val col = reopenCol() // test that the changes are physically saved to the DB @@ -211,7 +218,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that fields were set correctly", addedNote.fields, - TEST_NOTE_FIELDS.toMutableList() + TEST_NOTE_FIELDS.toMutableList(), ) assertEquals("Check that tag was set correctly", TEST_TAG, addedNote.tags[0]) val model: JSONObject? = col.notetypes.get(mModelId) @@ -235,11 +242,12 @@ class ContentProviderTest : InstrumentedTest() { @KotlinCleanup("assertThrows") fun testInsertNoteWithBadModelId() { val invalidModelId = 12 - val values = ContentValues().apply { - put(FlashCardsContract.Note.MID, invalidModelId) - put(FlashCardsContract.Note.FLDS, Utils.joinFields(TEST_NOTE_FIELDS)) - put(FlashCardsContract.Note.TAGS, TEST_TAG) - } + val values = + ContentValues().apply { + put(FlashCardsContract.Note.MID, invalidModelId) + put(FlashCardsContract.Note.FLDS, Utils.joinFields(TEST_NOTE_FIELDS)) + put(FlashCardsContract.Note.TAGS, TEST_TAG) + } try { contentResolver.insert(FlashCardsContract.Note.CONTENT_URI, values) fail() @@ -265,13 +273,14 @@ class ContentProviderTest : InstrumentedTest() { val testIndex = TEST_MODEL_CARDS.size - 1 // choose the last one because not the same as the basic model template val expectedOrd = model.getJSONArray("tmpls").length() - val cv = ContentValues().apply { - put(FlashCardsContract.CardTemplate.NAME, TEST_MODEL_CARDS[testIndex]) - put(FlashCardsContract.CardTemplate.QUESTION_FORMAT, TEST_MODEL_QFMT[testIndex]) - put(FlashCardsContract.CardTemplate.ANSWER_FORMAT, TEST_MODEL_AFMT[testIndex]) - put(FlashCardsContract.CardTemplate.BROWSER_QUESTION_FORMAT, TEST_MODEL_QFMT[testIndex]) - put(FlashCardsContract.CardTemplate.BROWSER_ANSWER_FORMAT, TEST_MODEL_AFMT[testIndex]) - } + val cv = + ContentValues().apply { + put(FlashCardsContract.CardTemplate.NAME, TEST_MODEL_CARDS[testIndex]) + put(FlashCardsContract.CardTemplate.QUESTION_FORMAT, TEST_MODEL_QFMT[testIndex]) + put(FlashCardsContract.CardTemplate.ANSWER_FORMAT, TEST_MODEL_AFMT[testIndex]) + put(FlashCardsContract.CardTemplate.BROWSER_QUESTION_FORMAT, TEST_MODEL_QFMT[testIndex]) + put(FlashCardsContract.CardTemplate.BROWSER_ANSWER_FORMAT, TEST_MODEL_AFMT[testIndex]) + } val templatesUri = Uri.withAppendedPath(modelUri, "templates") val templateUri = cr.insert(templatesUri, cv) col = reopenCol() // test that the changes are physically saved to the DB @@ -280,8 +289,8 @@ class ContentProviderTest : InstrumentedTest() { "Check template uri ord", expectedOrd.toLong(), ContentUris.parseId( - templateUri!! - ) + templateUri!!, + ), ) model = col.notetypes.get(modelId) assertNotNull("Check model", model) @@ -289,12 +298,12 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check template JSONObject ord", expectedOrd, - template.getInt("ord") + template.getInt("ord"), ) assertEquals( "Check template name", TEST_MODEL_CARDS[testIndex], - template.getString("name") + template.getString("name"), ) assertEquals("Check qfmt", TEST_MODEL_QFMT[testIndex], template.getString("qfmt")) assertEquals("Check afmt", TEST_MODEL_AFMT[testIndex], template.getString("afmt")) @@ -332,12 +341,12 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check fields length", (initialFieldCount + 1), - fldsArr.length() + fldsArr.length(), ) assertEquals( "Check last field name", TEST_FIELD_NAME, - fldsArr.getJSONObject(fldsArr.length() - 1).optString("name", "") + fldsArr.getJSONObject(fldsArr.length() - 1).optString("name", ""), ) col.notetypes.rem(model) } @@ -354,13 +363,13 @@ class ContentProviderTest : InstrumentedTest() { null, "mid=$mModelId", null, - null + null, ).use { cursor -> assertNotNull(cursor) assertEquals( "Check number of results", mCreatedNotes.size, - cursor.count + cursor.count, ) } // search for bogus mid @@ -388,7 +397,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check number of results", mCreatedNotes.size, - it.count + it.count, ) while (it.moveToNext()) { // Check that it's possible to leave out columns from the projection @@ -401,28 +410,28 @@ class ContentProviderTest : InstrumentedTest() { cr.query(noteUri, projection, null, null, null).use { singleNoteCursor -> assertNotNull( "Check that there is a valid cursor for detail data", - singleNoteCursor + singleNoteCursor, ) assertEquals( "Check that there is exactly one result", 1, - singleNoteCursor!!.count + singleNoteCursor!!.count, ) assertTrue( "Move to beginning of cursor after querying for detail data", - singleNoteCursor.moveToFirst() + singleNoteCursor.moveToFirst(), ) // Check columns assertEquals( "Check column count", projection.size, - singleNoteCursor.columnCount + singleNoteCursor.columnCount, ) for (j in projection.indices) { assertEquals( "Check column name $j", projection[j], - singleNoteCursor.getColumnName(j) + singleNoteCursor.getColumnName(j), ) } } @@ -445,25 +454,25 @@ class ContentProviderTest : InstrumentedTest() { projection, "tag:$TEST_TAG", null, - null + null, ).use { allNotesCursor -> assertNotNull("Check that there is a valid cursor", allNotesCursor) assertEquals( "Check number of results", mCreatedNotes.size, - allNotesCursor!!.count + allNotesCursor!!.count, ) // Check columns assertEquals( "Check column count", projection.size, - allNotesCursor.columnCount + allNotesCursor.columnCount, ) for (j in projection.indices) { assertEquals( "Check column name $j", projection[j], - allNotesCursor.getColumnName(j) + allNotesCursor.getColumnName(j), ) } } @@ -471,7 +480,10 @@ class ContentProviderTest : InstrumentedTest() { } @Suppress("SameParameterValue") - private fun removeFromProjection(inputProjection: Array, idx: Int): Array { + private fun removeFromProjection( + inputProjection: Array, + idx: Int, + ): Array { val outputProjection = arrayOfNulls(inputProjection.size - 1) if (idx >= 0) { System.arraycopy(inputProjection, 0, outputProjection, 0, idx) @@ -501,21 +513,22 @@ class ContentProviderTest : InstrumentedTest() { .use { noteCursor -> assertNotNull( "Check that there is a valid cursor for detail data after update", - noteCursor + noteCursor, ) assertEquals( "Check that there is one and only one entry after update", 1, - noteCursor!!.count + noteCursor!!.count, ) assertTrue("Move to first item in cursor", noteCursor.moveToFirst()) - val newFields = Utils.splitFields( - noteCursor.getString(noteCursor.getColumnIndex(FlashCardsContract.Note.FLDS)) - ) + val newFields = + Utils.splitFields( + noteCursor.getString(noteCursor.getColumnIndex(FlashCardsContract.Note.FLDS)), + ) assertEquals( "Check that the flds have been updated correctly", newFields, - dummyFields2.toMutableList() + dummyFields2.toMutableList(), ) } } @@ -527,12 +540,13 @@ class ContentProviderTest : InstrumentedTest() { @Test fun testInsertAndUpdateModel() { val cr = contentResolver - var cv = ContentValues().apply { - // Insert a new model - put(FlashCardsContract.Model.NAME, TEST_MODEL_NAME) - put(FlashCardsContract.Model.FIELD_NAMES, Utils.joinFields(TEST_MODEL_FIELDS)) - put(FlashCardsContract.Model.NUM_CARDS, TEST_MODEL_CARDS.size) - } + var cv = + ContentValues().apply { + // Insert a new model + put(FlashCardsContract.Model.NAME, TEST_MODEL_NAME) + put(FlashCardsContract.Model.FIELD_NAMES, Utils.joinFields(TEST_MODEL_FIELDS)) + put(FlashCardsContract.Model.NUM_CARDS, TEST_MODEL_CARDS.size) + } val modelUri = cr.insert(FlashCardsContract.Model.CONTENT_URI, cv) assertNotNull("Check inserted model isn't null", modelUri) assertNotNull("Check last path segment exists", modelUri!!.lastPathSegment) @@ -545,19 +559,19 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check templates length", TEST_MODEL_CARDS.size, - model.getJSONArray("tmpls").length() + model.getJSONArray("tmpls").length(), ) assertEquals( "Check field length", TEST_MODEL_FIELDS.size, - model.getJSONArray("flds").length() + model.getJSONArray("flds").length(), ) val fields = model.getJSONArray("flds") for (i in 0 until fields.length()) { assertEquals( "Check name of fields", TEST_MODEL_FIELDS[i], - fields.getJSONObject(i).getString("name") + fields.getJSONObject(i).getString("name"), ) } // Test updating the model CSS (to test updating MODELS_ID Uri) @@ -565,7 +579,7 @@ class ContentProviderTest : InstrumentedTest() { cv.put(FlashCardsContract.Model.CSS, TEST_MODEL_CSS) assertThat( cr.update(modelUri, cv, null, null), - greaterThan(0) + greaterThan(0), ) col = reopenCol() model = col.notetypes.get(mid) @@ -573,21 +587,23 @@ class ContentProviderTest : InstrumentedTest() { assertEquals("Check css", TEST_MODEL_CSS, model!!.getString("css")) // Update each of the templates in model (to test updating MODELS_ID_TEMPLATES_ID Uri) for (i in TEST_MODEL_CARDS.indices) { - cv = ContentValues().apply { - put(FlashCardsContract.CardTemplate.NAME, TEST_MODEL_CARDS[i]) - put(FlashCardsContract.CardTemplate.QUESTION_FORMAT, TEST_MODEL_QFMT[i]) - put(FlashCardsContract.CardTemplate.ANSWER_FORMAT, TEST_MODEL_AFMT[i]) - put(FlashCardsContract.CardTemplate.BROWSER_QUESTION_FORMAT, TEST_MODEL_QFMT[i]) - put(FlashCardsContract.CardTemplate.BROWSER_ANSWER_FORMAT, TEST_MODEL_AFMT[i]) - } - val tmplUri = Uri.withAppendedPath( - Uri.withAppendedPath(modelUri, "templates"), - i.toString() - ) + cv = + ContentValues().apply { + put(FlashCardsContract.CardTemplate.NAME, TEST_MODEL_CARDS[i]) + put(FlashCardsContract.CardTemplate.QUESTION_FORMAT, TEST_MODEL_QFMT[i]) + put(FlashCardsContract.CardTemplate.ANSWER_FORMAT, TEST_MODEL_AFMT[i]) + put(FlashCardsContract.CardTemplate.BROWSER_QUESTION_FORMAT, TEST_MODEL_QFMT[i]) + put(FlashCardsContract.CardTemplate.BROWSER_ANSWER_FORMAT, TEST_MODEL_AFMT[i]) + } + val tmplUri = + Uri.withAppendedPath( + Uri.withAppendedPath(modelUri, "templates"), + i.toString(), + ) assertThat( "Update rows", cr.update(tmplUri, cv, null, null), - greaterThan(0) + greaterThan(0), ) col = reopenCol() model = col.notetypes.get(mid) @@ -596,7 +612,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check template name", TEST_MODEL_CARDS[i], - template.getString("name") + template.getString("name"), ) assertEquals("Check qfmt", TEST_MODEL_QFMT[i], template.getString("qfmt")) assertEquals("Check afmt", TEST_MODEL_AFMT[i], template.getString("afmt")) @@ -629,22 +645,23 @@ class ContentProviderTest : InstrumentedTest() { assertThat( "Check that there is at least one result", allModels.count, - greaterThan(0) + greaterThan(0), ) while (allModels.moveToNext()) { val modelId = allModels.getLong(allModels.getColumnIndex(FlashCardsContract.Model._ID)) - val modelUri = Uri.withAppendedPath( - FlashCardsContract.Model.CONTENT_URI, - modelId.toString() - ) + val modelUri = + Uri.withAppendedPath( + FlashCardsContract.Model.CONTENT_URI, + modelId.toString(), + ) val singleModel = cr.query(modelUri, null, null, null, null) assertNotNull(singleModel) singleModel.use { assertEquals( "Check that there is exactly one result", 1, - it.count + it.count, ) assertTrue("Move to beginning of cursor", it.moveToFirst()) val nameFromModels = @@ -654,21 +671,21 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that model names are the same", nameFromModel, - nameFromModels + nameFromModels, ) val flds = allModels.getString(allModels.getColumnIndex(FlashCardsContract.Model.FIELD_NAMES)) assertThat( "Check that valid number of fields", Utils.splitFields(flds).size, - greaterThanOrEqualTo(1) + greaterThanOrEqualTo(1), ) val numCards = allModels.getInt(allModels.getColumnIndex(FlashCardsContract.Model.NUM_CARDS)) assertThat( "Check that valid number of cards", numCards, - greaterThanOrEqualTo(1) + greaterThanOrEqualTo(1), ) } } @@ -689,46 +706,48 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check number of results", mCreatedNotes.size, - it.count + it.count, ) while (it.moveToNext()) { // Now iterate over all cursors - val cardsUri = Uri.withAppendedPath( + val cardsUri = Uri.withAppendedPath( - FlashCardsContract.Note.CONTENT_URI, - it.getString(it.getColumnIndex(FlashCardsContract.Note._ID)) - ), - "cards" - ) + Uri.withAppendedPath( + FlashCardsContract.Note.CONTENT_URI, + it.getString(it.getColumnIndex(FlashCardsContract.Note._ID)), + ), + "cards", + ) cr.query(cardsUri, null, null, null, null).use { cardsCursor -> assertNotNull( "Check that there is a valid cursor after query for cards", - cardsCursor + cardsCursor, ) assertThat( "Check that there is at least one result for cards", cardsCursor!!.count, - greaterThan(0) + greaterThan(0), ) while (cardsCursor.moveToNext()) { val targetDid = mTestDeckIds[0] // Move to test deck (to test NOTES_ID_CARDS_ORD Uri) val values = ContentValues() values.put(FlashCardsContract.Card.DECK_ID, targetDid) - val cardUri = Uri.withAppendedPath( - cardsUri, - cardsCursor.getString(cardsCursor.getColumnIndex(FlashCardsContract.Card.CARD_ORD)) - ) + val cardUri = + Uri.withAppendedPath( + cardsUri, + cardsCursor.getString(cardsCursor.getColumnIndex(FlashCardsContract.Card.CARD_ORD)), + ) cr.update(cardUri, values, null, null) reopenCol() val movedCardCur = cr.query(cardUri, null, null, null, null) assertNotNull( "Check that there is a valid cursor after moving card", - movedCardCur + movedCardCur, ) assertTrue( "Move to beginning of cursor after moving card", - movedCardCur!!.moveToFirst() + movedCardCur!!.moveToFirst(), ) val did = movedCardCur.getLong(movedCardCur.getColumnIndex(FlashCardsContract.Card.DECK_ID)) @@ -745,26 +764,27 @@ class ContentProviderTest : InstrumentedTest() { @Test fun testQueryCurrentModel() { val cr = contentResolver - val uri = Uri.withAppendedPath( - FlashCardsContract.Model.CONTENT_URI, - FlashCardsContract.Model.CURRENT_MODEL_ID - ) + val uri = + Uri.withAppendedPath( + FlashCardsContract.Model.CONTENT_URI, + FlashCardsContract.Model.CURRENT_MODEL_ID, + ) val modelCursor = cr.query(uri, null, null, null, null) assertNotNull(modelCursor) modelCursor.use { assertEquals( "Check that there is exactly one result", 1, - it.count + it.count, ) assertTrue("Move to beginning of cursor", it.moveToFirst()) assertNotNull( "Check non-empty field names", - it.getString(it.getColumnIndex(FlashCardsContract.Model.FIELD_NAMES)) + it.getString(it.getColumnIndex(FlashCardsContract.Model.FIELD_NAMES)), ) assertTrue( "Check at least one template", - it.getInt(it.getColumnIndex(FlashCardsContract.Model.NUM_CARDS)) > 0 + it.getInt(it.getColumnIndex(FlashCardsContract.Model.NUM_CARDS)) > 0, ) } } @@ -777,15 +797,17 @@ class ContentProviderTest : InstrumentedTest() { fun testUnsupportedOperations() { val cr = contentResolver val dummyValues = ContentValues() - val updateUris = arrayOf( // Can't update most tables in bulk -- only via ID - FlashCardsContract.Note.CONTENT_URI, - FlashCardsContract.Model.CONTENT_URI, - FlashCardsContract.Deck.CONTENT_ALL_URI, - FlashCardsContract.Note.CONTENT_URI.buildUpon() - .appendPath("1234") - .appendPath("cards") - .build() - ) + val updateUris = + arrayOf( + // Can't update most tables in bulk -- only via ID + FlashCardsContract.Note.CONTENT_URI, + FlashCardsContract.Model.CONTENT_URI, + FlashCardsContract.Deck.CONTENT_ALL_URI, + FlashCardsContract.Note.CONTENT_URI.buildUpon() + .appendPath("1234") + .appendPath("cards") + .build(), + ) for (uri in updateUris) { try { cr.update(uri, dummyValues, null, null) @@ -796,22 +818,24 @@ class ContentProviderTest : InstrumentedTest() { // ... or this. } } - val deleteUris = arrayOf( - FlashCardsContract.Note.CONTENT_URI, // Only note/ is supported - FlashCardsContract.Note.CONTENT_URI.buildUpon() - .appendPath("1234") - .appendPath("cards") - .build(), - FlashCardsContract.Note.CONTENT_URI.buildUpon() - .appendPath("1234") - .appendPath("cards") - .appendPath("2345") - .build(), - FlashCardsContract.Model.CONTENT_URI, - FlashCardsContract.Model.CONTENT_URI.buildUpon() - .appendPath("1234") - .build() - ) + val deleteUris = + arrayOf( + // Only note/ is supported + FlashCardsContract.Note.CONTENT_URI, + FlashCardsContract.Note.CONTENT_URI.buildUpon() + .appendPath("1234") + .appendPath("cards") + .build(), + FlashCardsContract.Note.CONTENT_URI.buildUpon() + .appendPath("1234") + .appendPath("cards") + .appendPath("2345") + .build(), + FlashCardsContract.Model.CONTENT_URI, + FlashCardsContract.Model.CONTENT_URI.buildUpon() + .appendPath("1234") + .build(), + ) for (uri in deleteUris) { try { cr.delete(uri, null, null) @@ -820,23 +844,25 @@ class ContentProviderTest : InstrumentedTest() { // This was expected } } - val insertUris = arrayOf( // Can't do an insert with specific ID on the following tables - FlashCardsContract.Note.CONTENT_URI.buildUpon() - .appendPath("1234") - .build(), - FlashCardsContract.Note.CONTENT_URI.buildUpon() - .appendPath("1234") - .appendPath("cards") - .build(), - FlashCardsContract.Note.CONTENT_URI.buildUpon() - .appendPath("1234") - .appendPath("cards") - .appendPath("2345") - .build(), - FlashCardsContract.Model.CONTENT_URI.buildUpon() - .appendPath("1234") - .build() - ) + val insertUris = + arrayOf( + // Can't do an insert with specific ID on the following tables + FlashCardsContract.Note.CONTENT_URI.buildUpon() + .appendPath("1234") + .build(), + FlashCardsContract.Note.CONTENT_URI.buildUpon() + .appendPath("1234") + .appendPath("cards") + .build(), + FlashCardsContract.Note.CONTENT_URI.buildUpon() + .appendPath("1234") + .appendPath("cards") + .appendPath("2345") + .build(), + FlashCardsContract.Model.CONTENT_URI.buildUpon() + .appendPath("1234") + .build(), + ) for (uri in insertUris) { try { cr.insert(uri, dummyValues) @@ -856,20 +882,21 @@ class ContentProviderTest : InstrumentedTest() { fun testQueryAllDecks() { val col = col val decks = col.decks - val decksCursor = contentResolver - .query( - FlashCardsContract.Deck.CONTENT_ALL_URI, - FlashCardsContract.Deck.DEFAULT_PROJECTION, - null, - null, - null - ) + val decksCursor = + contentResolver + .query( + FlashCardsContract.Deck.CONTENT_ALL_URI, + FlashCardsContract.Deck.DEFAULT_PROJECTION, + null, + null, + null, + ) assertNotNull(decksCursor) decksCursor.use { assertEquals( "Check number of results", decks.count(), - it.count + it.count, ) while (it.moveToNext()) { val deckID = @@ -881,7 +908,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that the received deck has the correct name", deck.getString("name"), - deckName + deckName, ) } } @@ -894,10 +921,11 @@ class ContentProviderTest : InstrumentedTest() { fun testQueryCertainDeck() { val col = col val deckId = mTestDeckIds[0] - val deckUri = Uri.withAppendedPath( - FlashCardsContract.Deck.CONTENT_ALL_URI, - deckId.toString() - ) + val deckUri = + Uri.withAppendedPath( + FlashCardsContract.Deck.CONTENT_ALL_URI, + deckId.toString(), + ) contentResolver.query(deckUri, null, null, null, null).use { decksCursor -> if (decksCursor == null || !decksCursor.moveToFirst()) { fail("No deck received. Should have delivered deck with id $deckId") @@ -910,12 +938,12 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that received deck ID equals real deck ID", deckId, - returnedDeckID + returnedDeckID, ) assertEquals( "Check that received deck name equals real deck name", realDeck.getString("name"), - returnedDeckName + returnedDeckName, ) } } @@ -928,13 +956,14 @@ class ContentProviderTest : InstrumentedTest() { fun testQueryNextCard() { val col = col val sched = col.sched - val reviewInfoCursor = contentResolver.query( - FlashCardsContract.ReviewInfo.CONTENT_URI, - null, - null, - null, - null - ) + val reviewInfoCursor = + contentResolver.query( + FlashCardsContract.ReviewInfo.CONTENT_URI, + null, + null, + null, + null, + ) assertNotNull(reviewInfoCursor) assertEquals("Check that we actually received one card", 1, reviewInfoCursor.count) reviewInfoCursor.moveToFirst() @@ -951,12 +980,12 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that received card and actual card have same note id", nextCard!!.note().id, - noteID + noteID, ) assertEquals( "Check that received card and actual card have same card ord", nextCard.ord, - cardOrd + cardOrd, ) } @@ -973,13 +1002,14 @@ class ContentProviderTest : InstrumentedTest() { val sched = col.sched val selectedDeckBeforeTest = col.decks.selected() col.decks.select(1) // select Default deck - val reviewInfoCursor = contentResolver.query( - FlashCardsContract.ReviewInfo.CONTENT_URI, - null, - deckSelector, - deckArguments, - null - ) + val reviewInfoCursor = + contentResolver.query( + FlashCardsContract.ReviewInfo.CONTENT_URI, + null, + deckSelector, + deckArguments, + null, + ) assertNotNull(reviewInfoCursor) assertEquals("Check that we actually received one card", 1, reviewInfoCursor.count) reviewInfoCursor.use { @@ -991,7 +1021,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that the selected deck has not changed", 1, - col.decks.selected() + col.decks.selected(), ) col.decks.select(deckToTest) var nextCard: Card? = null @@ -1008,12 +1038,12 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that received card and actual card have same note id", nextCard!!.note().id, - noteID + noteID, ) assertEquals( "Check that received card and actual card have same card ord", nextCard.ord, - cardOrd + cardOrd, ) } col.decks.select(selectedDeckBeforeTest) @@ -1034,7 +1064,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that the selected deck has been correctly set", deckId, - col.decks.selected() + col.decks.selected(), ) } @@ -1060,18 +1090,20 @@ class ContentProviderTest : InstrumentedTest() { val noteId = card.note().id val cardOrd = card.ord val earlyGraduatingEase = AbstractFlashcardViewer.EASE_4 - val values = ContentValues().apply { - val timeTaken: Long = 5000 // 5 seconds - put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) - put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) - put(FlashCardsContract.ReviewInfo.EASE, earlyGraduatingEase) - put(FlashCardsContract.ReviewInfo.TIME_TAKEN, timeTaken) - } + val values = + ContentValues().apply { + val timeTaken: Long = 5000 // 5 seconds + put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) + put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) + put(FlashCardsContract.ReviewInfo.EASE, earlyGraduatingEase) + put(FlashCardsContract.ReviewInfo.TIME_TAKEN, timeTaken) + } val updateCount = cr.update(reviewInfoUri, values, null, null) assertEquals("Check if update returns 1", 1, updateCount) try { Thread.currentThread().join(500) - } catch (e: Exception) { /* do nothing */ + } catch (e: Exception) { + // do nothing } val newCard = col.sched.card if (newCard != null) { @@ -1099,7 +1131,7 @@ class ContentProviderTest : InstrumentedTest() { assertNotEquals( "Card is not user-buried before test", Consts.QUEUE_TYPE_SIBLING_BURIED, - card!!.queue + card!!.queue, ) // retain the card id, we will lookup the card after the update @@ -1112,11 +1144,12 @@ class ContentProviderTest : InstrumentedTest() { val noteId = card.note().id val cardOrd = card.ord val bury = 1 - val values = ContentValues().apply { - put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) - put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) - put(FlashCardsContract.ReviewInfo.BURY, bury) - } + val values = + ContentValues().apply { + put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) + put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) + put(FlashCardsContract.ReviewInfo.BURY, bury) + } val updateCount = cr.update(reviewInfoUri, values, null, null) assertEquals("Check if update returns 1", 1, updateCount) @@ -1127,7 +1160,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Card is user-buried", Consts.QUEUE_TYPE_MANUALLY_BURIED, - cardAfterUpdate.queue + cardAfterUpdate.queue, ) // cleanup, unbury cards @@ -1149,7 +1182,7 @@ class ContentProviderTest : InstrumentedTest() { assertNotEquals( "Card is not suspended before test", Consts.QUEUE_TYPE_SUSPENDED, - card!!.queue + card!!.queue, ) // retain the card id, we will lookup the card after the update @@ -1163,12 +1196,13 @@ class ContentProviderTest : InstrumentedTest() { val cardOrd = card.ord @KotlinCleanup("rename, while valid suspend is a kotlin soft keyword") - val values = ContentValues().apply { - val suspend = 1 - put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) - put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) - put(FlashCardsContract.ReviewInfo.SUSPEND, suspend) - } + val values = + ContentValues().apply { + val suspend = 1 + put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) + put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) + put(FlashCardsContract.ReviewInfo.SUSPEND, suspend) + } val updateCount = cr.update(reviewInfoUri, values, null, null) assertEquals("Check if update returns 1", 1, updateCount) @@ -1204,10 +1238,11 @@ class ContentProviderTest : InstrumentedTest() { // ----------- val tag2 = "mynewtag" val cr = contentResolver - val updateNoteUri = Uri.withAppendedPath( - FlashCardsContract.Note.CONTENT_URI, - noteId.toString() - ) + val updateNoteUri = + Uri.withAppendedPath( + FlashCardsContract.Note.CONTENT_URI, + noteId.toString(), + ) val values = ContentValues() values.put(FlashCardsContract.Note.TAGS, "$TEST_TAG $tag2") val updateCount = cr.update(updateNoteUri, values, null, null) @@ -1227,7 +1262,7 @@ class ContentProviderTest : InstrumentedTest() { fun testProviderProvidesDefaultForEmptyModelDeck() { assumeTrue( "This causes mild data corruption - should not be run on a collection you care about", - isEmulator() + isEmulator(), ) val col = col col.notetypes.all()[0].put("did", JSONObject.NULL) @@ -1250,23 +1285,29 @@ class ContentProviderTest : InstrumentedTest() { val note = addNoteUsingBasicModel("Hello$sound", back) val ord = 0 - val noteUri = Uri.withAppendedPath( - FlashCardsContract.Note.CONTENT_URI, - note.id.toString() - ) + val noteUri = + Uri.withAppendedPath( + FlashCardsContract.Note.CONTENT_URI, + note.id.toString(), + ) val cardsUri = Uri.withAppendedPath(noteUri, "cards") val specificCardUri = Uri.withAppendedPath(cardsUri, ord.toString()) contentResolver.query( specificCardUri, - arrayOf(FlashCardsContract.Card.QUESTION, FlashCardsContract.Card.ANSWER), // projection - null, // selection is ignored for this URI - null, // selectionArgs is ignored for this URI - null // sortOrder is ignored for this URI +// projection + arrayOf(FlashCardsContract.Card.QUESTION, FlashCardsContract.Card.ANSWER), + // selection is ignored for this URI + null, +// selectionArgs is ignored for this URI + null, +// sortOrder is ignored for this URI + null, )?.let { cursor -> if (!cursor.moveToFirst()) { fail("no rows in cursor") } + fun getString(id: String) = cursor.getString(cursor.getColumnIndex(id)) val question = getString(FlashCardsContract.Card.QUESTION) val answer = getString(FlashCardsContract.Card.ANSWER) @@ -1291,14 +1332,15 @@ class ContentProviderTest : InstrumentedTest() { private const val TEST_TAG = "aldskfhewjklhfczmxkjshf" // In case of change in TEST_DECKS, change mTestDeckIds for efficiency - private val TEST_DECKS = arrayOf( - "cmxieunwoogyxsctnjmv", - "sstuljxgmfdyugiujyhq", - "pdsqoelhmemmmbwjunnu", - "scxipjiyozczaaczoawo", - "cmxieunwoogyxsctnjmv::abcdefgh::ZYXW", - "cmxieunwoogyxsctnjmv::INSBGDS" - ) + private val TEST_DECKS = + arrayOf( + "cmxieunwoogyxsctnjmv", + "sstuljxgmfdyugiujyhq", + "pdsqoelhmemmmbwjunnu", + "scxipjiyozczaaczoawo", + "cmxieunwoogyxsctnjmv::abcdefgh::ZYXW", + "cmxieunwoogyxsctnjmv::INSBGDS", + ) private const val TEST_MODEL_NAME = "com.ichi2.anki.provider.test.a1x6h9l" private val TEST_MODEL_FIELDS = arrayOf("FRONTS", "BACK") private val TEST_MODEL_CARDS = arrayOf("cArD1", "caRD2") @@ -1313,7 +1355,7 @@ class ContentProviderTest : InstrumentedTest() { mid: Long, did: Long, fields: Array, - tag: String + tag: String, ): Uri { val newNote = Note(col, col.notetypes.get(mid)!!) for (idx in fields.indices) { @@ -1323,7 +1365,7 @@ class ContentProviderTest : InstrumentedTest() { assertThat( "At least one card added for note", col.addNote(newNote), - greaterThanOrEqualTo(1) + greaterThanOrEqualTo(1), ) for (c in newNote.cards()) { c.did = did @@ -1331,7 +1373,7 @@ class ContentProviderTest : InstrumentedTest() { } return Uri.withAppendedPath( FlashCardsContract.Note.CONTENT_URI, - newNote.id.toString() + newNote.id.toString(), ) } } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt index 0f73847d255e..ef4b09b60959 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt @@ -89,7 +89,7 @@ abstract class InstrumentedTest { Build.PRODUCT.contains("vbox86p") || Build.PRODUCT.contains("emulator") || Build.PRODUCT.contains("simulator") - ) + ) } } @@ -153,14 +153,21 @@ abstract class InstrumentedTest { } @DuplicatedCode("This is copied from RobolectricTest. This will be refactored into a shared library later") - internal fun addNoteUsingBasicModel(front: String = "Front", back: String = "Back"): Note { + internal fun addNoteUsingBasicModel( + front: String = "Front", + back: String = "Back", + ): Note { return addNoteUsingModelName("Basic", front, back) } @DuplicatedCode("This is copied from RobolectricTest. This will be refactored into a shared library later") - private fun addNoteUsingModelName(name: String, vararg fields: String): Note { - val model = col.notetypes.byName(name) - ?: throw IllegalArgumentException("Could not find model '$name'") + private fun addNoteUsingModelName( + name: String, + vararg fields: String, + ): Note { + val model = + col.notetypes.byName(name) + ?: throw IllegalArgumentException("Could not find model '$name'") // PERF: if we modify newNote(), we can return the card and return a Pair here. // Saves a database trip afterwards. val n = col.newNote(model) @@ -174,7 +181,7 @@ abstract class InstrumentedTest { protected fun ViewInteraction.checkWithTimeout( viewAssertion: ViewAssertion, retryWaitTimeInMilliseconds: Long = 100, - maxWaitTimeInMilliseconds: Long = TimeUnit.SECONDS.toMillis(10) + maxWaitTimeInMilliseconds: Long = TimeUnit.SECONDS.toMillis(10), ) { val startTime = TimeManager.time.intTimeMS() diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/LayoutValidationTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/LayoutValidationTest.kt index 2d5f1b3d611c..4510ca3d2291 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/LayoutValidationTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/LayoutValidationTest.kt @@ -82,7 +82,7 @@ class LayoutValidationTest : InstrumentedTest() { @Throws( IllegalAccessException::class, InvocationTargetException::class, - InstantiationException::class + InstantiationException::class, ) @JvmStatic // required for initParameters fun initParameters(): Collection> { @@ -101,10 +101,11 @@ class LayoutValidationTest : InstrumentedTest() { // with a specified fragment name, as these would currently fail the test, throwing: // UnsupportedOperationException: FragmentContainerView must be within // a FragmentActivity to use android:name="..." - val ignoredLayoutIds = listOf( - com.ichi2.anki.R.layout.activity_manage_space, - com.ichi2.anki.R.layout.introduction_activity - ) + val ignoredLayoutIds = + listOf( + com.ichi2.anki.R.layout.activity_manage_space, + com.ichi2.anki.R.layout.introduction_activity, + ) return layout::class.java.fields .map { arrayOf(it.getInt(layout), it.name) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/NotificationChannelTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/NotificationChannelTest.kt index 9b3c47d05971..8ce004008808 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/NotificationChannelTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/NotificationChannelTest.kt @@ -87,12 +87,12 @@ class NotificationChannelTest : InstrumentedTest() { assertThat( "Not as many channels as expected.", expectedChannels, - greaterThanOrEqualTo(channels.size) + greaterThanOrEqualTo(channels.size), ) for (channel in Channel.entries) { assertNotNull( "There should be a reminder channel", - mManager!!.getNotificationChannel(channel.id) + mManager!!.getNotificationChannel(channel.id), ) } } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt index e20c2bfc14e7..3dbcac6ea0a2 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt @@ -51,22 +51,27 @@ object Shared { * @param name An additional suffix to ensure the test directory is only used by a particular resource. * @return See getTestDir. */ - private fun getTestDir(context: Context, name: String): File { - val suffix = if (name.isNotEmpty()) { - "-$name" - } else { - "" - } + private fun getTestDir( + context: Context, + name: String, + ): File { + val suffix = + if (name.isNotEmpty()) { + "-$name" + } else { + "" + } val dir = File(context.cacheDir, "testfiles$suffix") if (!dir.exists()) { assertTrue(dir.mkdir()) } - val files = dir.listFiles() - ?: // Had this problem on an API 16 emulator after a stress test - directory existed - // but listFiles() returned null due to EMFILE (Too many open files) - // Don't throw here - later file accesses will provide a better exception. - // and the directory exists, even if it's unusable. - return dir + val files = + dir.listFiles() + ?: // Had this problem on an API 16 emulator after a stress test - directory existed + // but listFiles() returned null due to EMFILE (Too many open files) + // Don't throw here - later file accesses will provide a better exception. + // and the directory exists, even if it's unusable. + return dir for (f in files) { assertTrue(f.delete()) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/DBTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/DBTest.kt index 47faa0b8445b..364227971314 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/DBTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/DBTest.kt @@ -48,13 +48,14 @@ class DBTest : InstrumentedTest() { SQLiteDatabase.deleteDatabase(illFatedDBFile) Assert.assertFalse("database exists already", illFatedDBFile.exists()) val callback = TestCallback(1) - val illFatedDB = DB( - AnkiSupportSQLiteDatabase.withFramework( - testContext, - illFatedDBFile.canonicalPath, - callback + val illFatedDB = + DB( + AnkiSupportSQLiteDatabase.withFramework( + testContext, + illFatedDBFile.canonicalPath, + callback, + ), ) - ) Assert.assertFalse("database should not be corrupt yet", callback.databaseIsCorrupt) // Scribble in it @@ -83,6 +84,7 @@ class DBTest : InstrumentedTest() { // Test fixture that lets us inspect corruption handler status inner class TestCallback(version: Int) : AnkiSupportSQLiteDatabase.DefaultDbCallback(version) { internal var databaseIsCorrupt = false + override fun onCorruption(db: SupportSQLiteDatabase) { databaseIsCorrupt = true super.onCorruption(db) diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt index 72df84228e3f..069526b227eb 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt @@ -131,7 +131,9 @@ class MediaTest : InstrumentedTest() { @Suppress("SpellCheckingInspection") @Throws(IOException::class) - private fun createNonEmptyFile(@Suppress("SameParameterValue") fileName: String): File { + private fun createNonEmptyFile( + @Suppress("SameParameterValue") fileName: String, + ): File { val file = File(testDir, fileName) FileOutputStream(file, false).use { os -> os.write("a".toByteArray()) } return file diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/NotetypeTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/NotetypeTest.kt index 761381821417..79a2767d357b 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/NotetypeTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/NotetypeTest.kt @@ -28,7 +28,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class NotetypeTest : InstrumentedTest() { - private val mTestCol = emptyCol @After @@ -40,7 +39,7 @@ class NotetypeTest : InstrumentedTest() { fun bigQuery() { assumeTrue( "This test is flaky on API29, ignoring", - Build.VERSION.SDK_INT != Build.VERSION_CODES.Q + Build.VERSION.SDK_INT != Build.VERSION_CODES.Q, ) val models = mTestCol.notetypes val model = models.all()[0] diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/RetryRule.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/RetryRule.kt index aecaece27bc7..3a32e8f2d129 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/RetryRule.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/RetryRule.kt @@ -44,11 +44,17 @@ class RetryRule(i: Int) : TestRule { mMaxTries = i } - override fun apply(base: Statement, description: Description): Statement { + override fun apply( + base: Statement, + description: Description, + ): Statement { return statement(base, description) } - private fun statement(base: Statement, description: Description): Statement { + private fun statement( + base: Statement, + description: Description, + ): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt index 221d60cd0f02..d97a39d08564 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt @@ -51,7 +51,7 @@ object DatabaseUtils { fun cursorFillWindow( cursor: Cursor, positionParam: Int, - window: CursorWindow + window: CursorWindow, ) { var position = positionParam if (position < 0 || position >= cursor.count) { @@ -68,23 +68,24 @@ object DatabaseUtils { break } for (i in 0 until numColumns) { - val success: Boolean = when (cursor.getType(i)) { - Cursor.FIELD_TYPE_NULL -> window.putNull(position, i) - Cursor.FIELD_TYPE_INTEGER -> window.putLong(cursor.getLong(i), position, i) - Cursor.FIELD_TYPE_FLOAT -> window.putDouble(cursor.getDouble(i), position, i) - Cursor.FIELD_TYPE_BLOB -> { - val value = cursor.getBlob(i) - if (value != null) window.putBlob(value, position, i) else window.putNull(position, i) + val success: Boolean = + when (cursor.getType(i)) { + Cursor.FIELD_TYPE_NULL -> window.putNull(position, i) + Cursor.FIELD_TYPE_INTEGER -> window.putLong(cursor.getLong(i), position, i) + Cursor.FIELD_TYPE_FLOAT -> window.putDouble(cursor.getDouble(i), position, i) + Cursor.FIELD_TYPE_BLOB -> { + val value = cursor.getBlob(i) + if (value != null) window.putBlob(value, position, i) else window.putNull(position, i) + } + Cursor.FIELD_TYPE_STRING -> { + val value = cursor.getString(i) + if (value != null) window.putString(value, position, i) else window.putNull(position, i) + } + else -> { + val value = cursor.getString(i) + if (value != null) window.putString(value, position, i) else window.putNull(position, i) + } } - Cursor.FIELD_TYPE_STRING -> { - val value = cursor.getString(i) - if (value != null) window.putString(value, position, i) else window.putNull(position, i) - } - else -> { - val value = cursor.getString(i) - if (value != null) window.putString(value, position, i) else window.putNull(position, i) - } - } if (!success) { window.freeLastRow() break@rowloop diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/GrantPermissions.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/GrantPermissions.kt index 3f5261ee4a23..2004aee4dd65 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/GrantPermissions.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/GrantPermissions.kt @@ -24,14 +24,15 @@ import org.junit.rules.TestRule object GrantStoragePermission { private val targetSdkVersion = InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.targetSdkVersion - val storagePermission = if ( - targetSdkVersion >= Build.VERSION_CODES.R && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - ) { - null - } else { - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - } + val storagePermission = + if ( + targetSdkVersion >= Build.VERSION_CODES.R && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + ) { + null + } else { + android.Manifest.permission.WRITE_EXTERNAL_STORAGE + } /** * Storage is longer necessary for API 30+ @@ -40,11 +41,12 @@ object GrantStoragePermission { val instance: TestRule = grantPermissions(storagePermission) } -val notificationPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - android.Manifest.permission.POST_NOTIFICATIONS -} else { - null -} +val notificationPermission = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + android.Manifest.permission.POST_NOTIFICATIONS + } else { + null + } /** Grants permissions, given some may be invalid */ fun grantPermissions(vararg permissions: String?): TestRule { diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/utils/GrantAllFilesAccessRule.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/utils/GrantAllFilesAccessRule.kt index c789e2adb74a..aa4ca08bce79 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/utils/GrantAllFilesAccessRule.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/utils/GrantAllFilesAccessRule.kt @@ -25,7 +25,10 @@ import org.junit.runner.Description import org.junit.runners.model.Statement class EnsureAllFilesAccessRule : TestRule { - override fun apply(base: Statement, description: Description): Statement { + override fun apply( + base: Statement, + description: Description, + ): Statement { ensureAllFilesAccess() return base } @@ -42,7 +45,7 @@ fun ensureAllFilesAccess() { throw IllegalStateException( "'All Files' access is required on your emulator/device. " + "Please grant it manually or change Build Variant to 'playDebug' in Android Studio " + - "(Build -> Select Build Variant)" + "(Build -> Select Build Variant)", ) } } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt b/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt index a2050beb1298..0dc8f1c70156 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt @@ -26,7 +26,11 @@ import androidx.test.runner.AndroidJUnitRunner */ @Suppress("unused") // referenced by build.gradle class NewCollectionPathTestRunner : AndroidJUnitRunner() { - override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context?, + ): Application { return super.newApplication(cl, TestingApplication::class.java.name, context) } } diff --git a/AnkiDroid/src/main/java/androidx/drawerlayout/widget/ClosableDrawerLayout.kt b/AnkiDroid/src/main/java/androidx/drawerlayout/widget/ClosableDrawerLayout.kt index c2401f4bdf6c..1fc132467ad5 100644 --- a/AnkiDroid/src/main/java/androidx/drawerlayout/widget/ClosableDrawerLayout.kt +++ b/AnkiDroid/src/main/java/androidx/drawerlayout/widget/ClosableDrawerLayout.kt @@ -38,7 +38,7 @@ class ClosableDrawerLayout : DrawerLayout { if (!mAnimationEnabled) { closeDrawer(GravityCompat.START, false); } - */ + */ super.closeDrawers(peekingOnly) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt b/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt index fdbe2a259766..b30728b28687 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt @@ -12,18 +12,23 @@ import kotlinx.parcelize.Parcelize object ActivityTransitionAnimation { @Suppress("DEPRECATION", "deprecated in API34 for predictive back, must plumb through new open/close parameter") - fun slide(activity: Activity, direction: Direction?) { + fun slide( + activity: Activity, + direction: Direction?, + ) { when (direction) { - Direction.START -> if (isRightToLeft(activity)) { - activity.overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out) - } else { - activity.overridePendingTransition(R.anim.slide_left_in, R.anim.slide_left_out) - } - Direction.END -> if (isRightToLeft(activity)) { - activity.overridePendingTransition(R.anim.slide_left_in, R.anim.slide_left_out) - } else { - activity.overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out) - } + Direction.START -> + if (isRightToLeft(activity)) { + activity.overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out) + } else { + activity.overridePendingTransition(R.anim.slide_left_in, R.anim.slide_left_out) + } + Direction.END -> + if (isRightToLeft(activity)) { + activity.overridePendingTransition(R.anim.slide_left_in, R.anim.slide_left_out) + } else { + activity.overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out) + } Direction.RIGHT -> activity.overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out) Direction.LEFT -> activity.overridePendingTransition(R.anim.slide_left_in, R.anim.slide_left_out) Direction.FADE -> activity.overridePendingTransition(R.anim.fade_out, R.anim.fade_in) @@ -37,10 +42,37 @@ object ActivityTransitionAnimation { } } - fun getAnimationOptions(activity: Activity, direction: Direction?): ActivityOptionsCompat { + fun getAnimationOptions( + activity: Activity, + direction: Direction?, + ): ActivityOptionsCompat { return when (direction) { - Direction.START -> if (isRightToLeft(activity)) ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_right_in, R.anim.slide_right_out) else ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_left_in, R.anim.slide_left_out) - Direction.END -> if (isRightToLeft(activity)) ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_left_in, R.anim.slide_left_out) else ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_right_in, R.anim.slide_right_out) + Direction.START -> + if (isRightToLeft( + activity, + ) + ) { + ActivityOptionsCompat.makeCustomAnimation( + activity, + R.anim.slide_right_in, + R.anim.slide_right_out, + ) + } else { + ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_left_in, R.anim.slide_left_out) + } + Direction.END -> + if (isRightToLeft( + activity, + ) + ) { + ActivityOptionsCompat.makeCustomAnimation( + activity, + R.anim.slide_left_in, + R.anim.slide_left_out, + ) + } else { + ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_right_in, R.anim.slide_right_out) + } Direction.RIGHT -> ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_right_in, R.anim.slide_right_out) Direction.LEFT -> ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_left_in, R.anim.slide_left_out) Direction.FADE -> ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.fade_out, R.anim.fade_in) @@ -59,7 +91,15 @@ object ActivityTransitionAnimation { @Parcelize enum class Direction : Parcelable { - START, END, FADE, UP, DOWN, RIGHT, LEFT, DEFAULT, NONE + START, + END, + FADE, + UP, + DOWN, + RIGHT, + LEFT, + DEFAULT, + NONE, } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 373e657e4e1e..33e0029e9f1d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -268,16 +268,17 @@ abstract class AbstractFlashcardViewer : private val mStartLongClickAction = Runnable { mGestureProcessor.onLongTap() } // Handler for the "show answer" button - private val mFlipCardListener = View.OnClickListener { - Timber.i("AbstractFlashcardViewer:: Show answer button pressed") - // Ignore what is most likely an accidental double-tap. - if (elapsedRealTime - lastClickTime < mDoubleTapTimeInterval) { - return@OnClickListener + private val mFlipCardListener = + View.OnClickListener { + Timber.i("AbstractFlashcardViewer:: Show answer button pressed") + // Ignore what is most likely an accidental double-tap. + if (elapsedRealTime - lastClickTime < mDoubleTapTimeInterval) { + return@OnClickListener + } + lastClickTime = elapsedRealTime + automaticAnswer.onShowAnswer() + displayCardAnswer() } - lastClickTime = elapsedRealTime - automaticAnswer.onShowAnswer() - displayCardAnswer() - } private val migrationService by migrationServiceWhileStartedOrNull() @@ -289,23 +290,24 @@ abstract class AbstractFlashcardViewer : */ private var refreshRequired: ViewerRefresh? = null - private val editCurrentCardLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - FlashCardViewerResultCallback { result, reloadRequired -> - if (result.resultCode == RESULT_OK) { - // content of note was changed so update the note and current card - Timber.i("AbstractFlashcardViewer:: Saving card...") - launchCatchingTask { saveEditedCard() } - onEditedNoteChanged() - } else if (result.resultCode == RESULT_CANCELED && !reloadRequired) { - // nothing was changed by the note editor so just redraw the card - redrawCard() - } - } - ) + private val editCurrentCardLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + FlashCardViewerResultCallback { result, reloadRequired -> + if (result.resultCode == RESULT_OK) { + // content of note was changed so update the note and current card + Timber.i("AbstractFlashcardViewer:: Saving card...") + launchCatchingTask { saveEditedCard() } + onEditedNoteChanged() + } else if (result.resultCode == RESULT_CANCELED && !reloadRequired) { + // nothing was changed by the note editor so just redraw the card + redrawCard() + } + }, + ) protected inner class FlashCardViewerResultCallback( - private val callback: (result: ActivityResult, reloadRequired: Boolean) -> Unit = { _, _ -> } + private val callback: (result: ActivityResult, reloadRequired: Boolean) -> Unit = { _, _ -> }, ) : ActivityResultCallback { override fun onActivityResult(result: ActivityResult) { if (result.resultCode == DeckPicker.RESULT_DB_ERROR) { @@ -338,7 +340,11 @@ abstract class AbstractFlashcardViewer : private var mHasBeenTouched = false private var mTouchX = 0f private var mTouchY = 0f - override fun onTouch(view: View, event: MotionEvent): Boolean { + + override fun onTouch( + view: View, + event: MotionEvent, + ): Boolean { if (event.action == MotionEvent.ACTION_DOWN) { // Save states when button pressed mPrevCard = currentCard @@ -411,20 +417,21 @@ abstract class AbstractFlashcardViewer : @get:VisibleForTesting protected open val elapsedRealTime: Long get() = SystemClock.elapsedRealtime() - private val mGestureListener = OnTouchListener { _, event -> - if (gestureDetector!!.onTouchEvent(event)) { - return@OnTouchListener true - } - if (!mGestureDetectorImpl.eventCanBeSentToWebView(event)) { - return@OnTouchListener false - } - // Gesture listener is added before mCard is set - processCardAction { cardWebView: WebView? -> - if (cardWebView == null) return@processCardAction - cardWebView.dispatchTouchEvent(event) + private val mGestureListener = + OnTouchListener { _, event -> + if (gestureDetector!!.onTouchEvent(event)) { + return@OnTouchListener true + } + if (!mGestureDetectorImpl.eventCanBeSentToWebView(event)) { + return@OnTouchListener false + } + // Gesture listener is added before mCard is set + processCardAction { cardWebView: WebView? -> + if (cardWebView == null) return@processCardAction + cardWebView.dispatchTouchEvent(event) + } + false } - false - } // This is intentionally package-private as it removes the need for synthetic accessors @SuppressLint("CheckResult") @@ -496,11 +503,12 @@ abstract class AbstractFlashcardViewer : open suspend fun updateCurrentCard() { // Legacy tests assume the current card will be grabbed from the collection, // despite that making no sense outside of Reviewer.kt - currentCard = withCol { - sched.card?.apply { - renderOutput() + currentCard = + withCol { + sched.card?.apply { + renderOutput() + } } - } } internal suspend fun updateCardAndRedraw() { @@ -545,10 +553,11 @@ abstract class AbstractFlashcardViewer : setContentView(getContentViewAttr(fullscreenMode)) - asyncCreateJob = launchCatchingTask { - val mediaDir = withCol { media.dir } - server = ReviewerServer(this@AbstractFlashcardViewer, mediaDir).apply { start() } - } + asyncCreateJob = + launchCatchingTask { + val mediaDir = withCol { media.dir } + server = ReviewerServer(this@AbstractFlashcardViewer, mediaDir).apply { start() } + } // Make ACTION_PROCESS_TEXT for in-app searching possible on > Android 4.0 delegate.isHandleNativeActionModesEnabled = true @@ -579,27 +588,29 @@ abstract class AbstractFlashcardViewer : public override fun onCollectionLoaded(col: Collection) { super.onCollectionLoaded(col) val mediaDir = col.media.dir - mBaseUrl = getBaseUrl(mediaDir).also { baseUrl -> - mSoundPlayer = Sound(baseUrl) - mViewerUrl = baseUrl + "__viewer__.html" - } - mAssetLoader = WebViewAssetLoader.Builder() - .addPathHandler("/") { path: String -> - try { - val file = File(mediaDir, path) - val inputStream = FileInputStream(file) - val mimeType = guessMimeType(path) - val headers = HashMap() - headers["Access-Control-Allow-Origin"] = "*" - val response = WebResourceResponse(mimeType, null, inputStream) - response.responseHeaders = headers - return@addPathHandler response - } catch (e: Exception) { - Timber.w(e, "Error trying to open path in asset loader") + mBaseUrl = + getBaseUrl(mediaDir).also { baseUrl -> + mSoundPlayer = Sound(baseUrl) + mViewerUrl = baseUrl + "__viewer__.html" + } + mAssetLoader = + WebViewAssetLoader.Builder() + .addPathHandler("/") { path: String -> + try { + val file = File(mediaDir, path) + val inputStream = FileInputStream(file) + val mimeType = guessMimeType(path) + val headers = HashMap() + headers["Access-Control-Allow-Origin"] = "*" + val response = WebResourceResponse(mimeType, null, inputStream) + response.responseHeaders = headers + return@addPathHandler response + } catch (e: Exception) { + Timber.w(e, "Error trying to open path in asset loader") + } + null } - null - } - .build() + .build() registerExternalStorageListener() restoreCollectionPreferences(col) initLayout() @@ -704,13 +715,16 @@ abstract class AbstractFlashcardViewer : } } - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyDown( + keyCode: Int, + event: KeyEvent, + ): Boolean { if (processCardFunction { cardWebView: WebView? -> - processHardwareButtonScroll( + processHardwareButtonScroll( keyCode, - cardWebView + cardWebView, ) - } + } ) { return true } @@ -731,7 +745,10 @@ abstract class AbstractFlashcardViewer : return super.onKeyDown(keyCode, event) } - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { if (webView.handledGamepadKeyUp(keyCode, event)) { return true } @@ -740,7 +757,10 @@ abstract class AbstractFlashcardViewer : public override val currentCardId: CardId? get() = currentCard?.id - private fun processHardwareButtonScroll(keyCode: Int, card: WebView?): Boolean { + private fun processHardwareButtonScroll( + keyCode: Int, + card: WebView?, + ): Boolean { if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { card!!.pageUp(false) if (mDoubleScrolling) { @@ -828,13 +848,17 @@ abstract class AbstractFlashcardViewer : */ private fun registerExternalStorageListener() { if (mUnmountReceiver == null) { - mUnmountReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == SdCardReceiver.MEDIA_EJECT) { - finish() + mUnmountReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (intent.action == SdCardReceiver.MEDIA_EJECT) { + finish() + } } } - } val iFilter = IntentFilter() iFilter.addAction(SdCardReceiver.MEDIA_EJECT) registerReceiver(mUnmountReceiver, iFilter) @@ -875,15 +899,16 @@ abstract class AbstractFlashcardViewer : title(R.string.delete_card_title) setIcon(R.drawable.ic_warning) message( - text = resources.getString( - R.string.delete_note_message, - Utils.stripHTML(currentCard!!.question(true)) - ) + text = + resources.getString( + R.string.delete_note_message, + Utils.stripHTML(currentCard!!.question(true)), + ), ) positiveButton(R.string.dialog_positive_delete) { Timber.i( "AbstractFlashcardViewer:: OK button pressed to delete note %d", - currentCard!!.nid + currentCard!!.nid, ) mSoundPlayer.stopSounds() deleteNoteWithoutConfirmation() @@ -896,23 +921,27 @@ abstract class AbstractFlashcardViewer : private fun deleteNoteWithoutConfirmation() { val cardId = currentCard!!.id launchCatchingTask { - val noteCount = withProgress() { - undoableOp { - removeNotes(cids = listOf(cardId)) - }.count - } - val deletedMessage = resources.getQuantityString( - R.plurals.card_browser_cards_deleted, - noteCount, - noteCount - ) + val noteCount = + withProgress { + undoableOp { + removeNotes(cids = listOf(cardId)) + }.count + } + val deletedMessage = + resources.getQuantityString( + R.plurals.card_browser_cards_deleted, + noteCount, + noteCount, + ) showSnackbar(deletedMessage, Snackbar.LENGTH_LONG) { setAction(R.string.undo) { launchCatchingTask { undoAndShowSnackbar() } } } } } - open fun answerCard(@BUTTON_TYPE ease: Int) { + open fun answerCard( + @BUTTON_TYPE ease: Int, + ) { launchCatchingTask { if (mInAnswer) { return@launchCatchingTask @@ -932,7 +961,9 @@ abstract class AbstractFlashcardViewer : } } - open suspend fun answerCardInner(@BUTTON_TYPE ease: Int) { + open suspend fun answerCardInner( + @BUTTON_TYPE ease: Int, + ) { // Legacy tests assume they can call answerCard() even outside of Reviewer withCol { sched.answerCard(currentCard!!, ease) @@ -952,30 +983,34 @@ abstract class AbstractFlashcardViewer : // Initialize swipe gestureDetector = GestureDetector(this, mGestureDetectorImpl) easeButtonsLayout = findViewById(R.id.ease_buttons) - easeButton1 = EaseButton( - EASE_1, - findViewById(R.id.flashcard_layout_ease1), - findViewById(R.id.ease1), - findViewById(R.id.nextTime1) - ).apply { setListeners(mEaseHandler) } - easeButton2 = EaseButton( - EASE_2, - findViewById(R.id.flashcard_layout_ease2), - findViewById(R.id.ease2), - findViewById(R.id.nextTime2) - ).apply { setListeners(mEaseHandler) } - easeButton3 = EaseButton( - EASE_3, - findViewById(R.id.flashcard_layout_ease3), - findViewById(R.id.ease3), - findViewById(R.id.nextTime3) - ).apply { setListeners(mEaseHandler) } - easeButton4 = EaseButton( - EASE_4, - findViewById(R.id.flashcard_layout_ease4), - findViewById(R.id.ease4), - findViewById(R.id.nextTime4) - ).apply { setListeners(mEaseHandler) } + easeButton1 = + EaseButton( + EASE_1, + findViewById(R.id.flashcard_layout_ease1), + findViewById(R.id.ease1), + findViewById(R.id.nextTime1), + ).apply { setListeners(mEaseHandler) } + easeButton2 = + EaseButton( + EASE_2, + findViewById(R.id.flashcard_layout_ease2), + findViewById(R.id.ease2), + findViewById(R.id.nextTime2), + ).apply { setListeners(mEaseHandler) } + easeButton3 = + EaseButton( + EASE_3, + findViewById(R.id.flashcard_layout_ease3), + findViewById(R.id.ease3), + findViewById(R.id.nextTime3), + ).apply { setListeners(mEaseHandler) } + easeButton4 = + EaseButton( + EASE_4, + findViewById(R.id.flashcard_layout_ease4), + findViewById(R.id.ease4), + findViewById(R.id.nextTime4), + ).apply { setListeners(mEaseHandler) } if (!mShowNextReviewTime) { easeButton1!!.hideNextReviewTime() easeButton2!!.hideNextReviewTime() @@ -1028,10 +1063,11 @@ abstract class AbstractFlashcardViewer : initControls() // Position answer buttons - val answerButtonsPosition = this.sharedPrefs().getString( - getString(R.string.answer_buttons_position_preference), - "bottom" - ) + val answerButtonsPosition = + this.sharedPrefs().getString( + getString(R.string.answer_buttons_position_preference), + "bottom", + ) mAnswerButtonsPosition = answerButtonsPosition val answerArea = findViewById(R.id.bottom_area_layout) val answerAreaParams = answerArea.layoutParams as RelativeLayout.LayoutParams @@ -1051,7 +1087,8 @@ abstract class AbstractFlashcardViewer : } "bottom", - "none" -> { + "none", + -> { whiteboardContainerParams.addRule(RelativeLayout.ABOVE, R.id.bottom_area_layout) whiteboardContainerParams.addRule(RelativeLayout.BELOW, R.id.mic_tool_bar_layer) flashcardContainerParams.addRule(RelativeLayout.ABOVE, R.id.bottom_area_layout) @@ -1101,7 +1138,7 @@ abstract class AbstractFlashcardViewer : Timber.d( "Focusable = %s, Focusable in touch mode = %s", webView.isFocusable, - webView.isFocusableInTouchMode + webView.isFocusableInTouchMode, ) webView.webViewClient = CardViewerWebClient(mAssetLoader, this) // Set transparent color to prevent flashing white when night mode enabled @@ -1139,10 +1176,13 @@ abstract class AbstractFlashcardViewer : processCardAction { cardWebView: WebView? -> cardWebView!!.loadUrl(url) } } - private fun inflateNewView(@IdRes id: Int): T { + private fun inflateNewView( + @IdRes id: Int, + ): T { val layoutId = getContentViewAttr(fullscreenMode) - val content = LayoutInflater.from(this@AbstractFlashcardViewer) - .inflate(layoutId, null, false) as ViewGroup + val content = + LayoutInflater.from(this@AbstractFlashcardViewer) + .inflate(layoutId, null, false) as ViewGroup val ret: T = content.findViewById(id) (ret!!.parent as ViewGroup).removeView(ret) // detach the view from its parent content.removeAllViews() @@ -1280,9 +1320,10 @@ abstract class AbstractFlashcardViewer : if (mGesturesEnabled) { mGestureProcessor.init(preferences) } - if (preferences.getBoolean("timeoutAnswer", false) || preferences.getBoolean( + if (preferences.getBoolean("timeoutAnswer", false) || + preferences.getBoolean( "keepScreenOn", - false + false, ) ) { this.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) @@ -1388,7 +1429,7 @@ abstract class AbstractFlashcardViewer : } Timber.i( "AbstractFlashcardViewer:: Question successfully shown for card id %d", - currentCard!!.id + currentCard!!.id, ) } @@ -1441,37 +1482,42 @@ abstract class AbstractFlashcardViewer : } } - override fun tapOnCurrentCard(x: Int, y: Int) { + override fun tapOnCurrentCard( + x: Int, + y: Int, + ) { // assemble suitable ACTION_DOWN and ACTION_UP events and forward them to the card's handler - val eDown = MotionEvent.obtain( - SystemClock.uptimeMillis(), - SystemClock.uptimeMillis(), - MotionEvent.ACTION_DOWN, - x.toFloat(), - y.toFloat(), - 1f, - 1f, - 0, - 1f, - 1f, - 0, - 0 - ) + val eDown = + MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_DOWN, + x.toFloat(), + y.toFloat(), + 1f, + 1f, + 0, + 1f, + 1f, + 0, + 0, + ) processCardAction { cardWebView: WebView? -> cardWebView!!.dispatchTouchEvent(eDown) } - val eUp = MotionEvent.obtain( - eDown.downTime, - SystemClock.uptimeMillis(), - MotionEvent.ACTION_UP, - x.toFloat(), - y.toFloat(), - 1f, - 1f, - 0, - 1f, - 1f, - 0, - 0 - ) + val eUp = + MotionEvent.obtain( + eDown.downTime, + SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, + x.toFloat(), + y.toFloat(), + 1f, + 1f, + 0, + 1f, + 1f, + 0, + 0, + ) processCardAction { cardWebView: WebView? -> cardWebView!!.dispatchTouchEvent(eUp) } } @@ -1511,8 +1557,8 @@ abstract class AbstractFlashcardViewer : FileOutputStream( File( CollectionHelper.getCurrentAnkiDroidDirectory(this), - "card.html" - ) + "card.html", + ), ).use { f -> f.write(cardContent!!.toByteArray()) } } catch (e: IOException) { Timber.d(e, "failed to save card") @@ -1522,10 +1568,11 @@ abstract class AbstractFlashcardViewer : playSounds(false) // Play sounds if appropriate } - private fun currentSideHasSounds(): Boolean = when (displayAnswer) { - false -> mSoundPlayer.hasQuestion() - true -> mSoundPlayer.hasAnswer() - } + private fun currentSideHasSounds(): Boolean = + when (displayAnswer) { + false -> mSoundPlayer.hasQuestion() + true -> mSoundPlayer.hasAnswer() + } /** * Plays sounds (or TTS, if configured) for currently shown side of card. @@ -1589,43 +1636,44 @@ abstract class AbstractFlashcardViewer : } private val soundErrorListener: Sound.OnErrorListener - get() = object : Sound.OnErrorListener { - private var handledError: HashSet = hashSetOf() - - override fun onError( - mp: MediaPlayer?, - which: Int, - extra: Int, - path: String? - ): ErrorHandling { - Timber.w("Media Error: (%d, %d). Calling OnCompletionListener", which, extra) - try { - val file = Uri.parse(path).toFile() - if (!file.exists()) { - if (handleStorageMigrationError(file)) { - return ErrorHandling.RETRY_AUDIO - } - mMissingImageHandler.processMissingSound(file) { filename: String? -> - displayCouldNotFindMediaSnackbar( - filename - ) + get() = + object : Sound.OnErrorListener { + private var handledError: HashSet = hashSetOf() + + override fun onError( + mp: MediaPlayer?, + which: Int, + extra: Int, + path: String?, + ): ErrorHandling { + Timber.w("Media Error: (%d, %d). Calling OnCompletionListener", which, extra) + try { + val file = Uri.parse(path).toFile() + if (!file.exists()) { + if (handleStorageMigrationError(file)) { + return ErrorHandling.RETRY_AUDIO + } + mMissingImageHandler.processMissingSound(file) { filename: String? -> + displayCouldNotFindMediaSnackbar( + filename, + ) + } } + } catch (e: Exception) { + Timber.w(e) } - } catch (e: Exception) { - Timber.w(e) + return ErrorHandling.CONTINUE_AUDIO } - return ErrorHandling.CONTINUE_AUDIO - } - private fun handleStorageMigrationError(file: File): Boolean { - val migrationService = migrationService ?: return false - if (handledError.contains(file.absolutePath)) { - return false + private fun handleStorageMigrationError(file: File): Boolean { + val migrationService = migrationService ?: return false + if (handledError.contains(file.absolutePath)) { + return false + } + handledError.add(file.absolutePath) + return migrationService.migrateFileImmediately(file) } - handledError.add(file.absolutePath) - return migrationService.migrateFileImmediately(file) } - } /** * Shows the dialogue for selecting TTS for the current card and cardside. @@ -1635,7 +1683,7 @@ abstract class AbstractFlashcardViewer : mTTS.selectTts( this, currentCard!!, - if (displayAnswer) SoundSide.ANSWER else SoundSide.QUESTION + if (displayAnswer) SoundSide.ANSWER else SoundSide.QUESTION, ) } } @@ -1655,7 +1703,10 @@ abstract class AbstractFlashcardViewer : } } - private fun loadContentIntoCard(card: WebView?, content: String) { + private fun loadContentIntoCard( + card: WebView?, + content: String, + ) { launchCatchingTask { asyncCreateJob?.join() server?.reviewerHtml = content @@ -1712,11 +1763,12 @@ abstract class AbstractFlashcardViewer : @VisibleForTesting open fun suspendNote(): Boolean { launchCatchingTask { - val changed = withProgress { - undoableOp { - sched.suspendNotes(listOf(currentCard!!.nid)) + val changed = + withProgress { + undoableOp { + sched.suspendNotes(listOf(currentCard!!.nid)) + } } - } val count = changed.count val noteSuspended = resources.getQuantityString(R.plurals.note_suspended, count, count) mSoundPlayer.stopSounds() @@ -1728,18 +1780,22 @@ abstract class AbstractFlashcardViewer : @VisibleForTesting open fun buryNote(): Boolean { launchCatchingTask { - val changed = withProgress { - undoableOp { - sched.buryNotes(listOf(currentCard!!.nid)) + val changed = + withProgress { + undoableOp { + sched.buryNotes(listOf(currentCard!!.nid)) + } } - } mSoundPlayer.stopSounds() showSnackbar(TR.studyingCardsBuried(changed.count), Reviewer.ACTION_SNACKBAR_TIME) } return true } - override fun executeCommand(which: ViewerCommand, fromGesture: Gesture?): Boolean { + override fun executeCommand( + which: ViewerCommand, + fromGesture: Gesture?, + ): Boolean { return when (which) { ViewerCommand.SHOW_ANSWER -> { if (displayAnswer) { @@ -1925,6 +1981,7 @@ abstract class AbstractFlashcardViewer : // ---------------------------------------------------------------------------- // INNER CLASSES // ---------------------------------------------------------------------------- + /** * Provides a hook for calling "alert" from javascript. Useful for debugging your javascript. */ @@ -1933,7 +1990,7 @@ abstract class AbstractFlashcardViewer : view: WebView, url: String, message: String, - result: JsResult + result: JsResult, ): Boolean { Timber.i("AbstractFlashcardViewer:: onJsAlert: %s", message) result.confirm() @@ -1987,7 +2044,7 @@ abstract class AbstractFlashcardViewer : } Timber.i( "AbstractFlashcardViewer:: Question successfully shown for card id %d", - currentCard!!.id + currentCard!!.id, ) } else { displayCardAnswer() @@ -2001,7 +2058,7 @@ abstract class AbstractFlashcardViewer : data: String, mimeType: String?, encoding: String?, - historyUrl: String? + historyUrl: String?, ) { if (!this@AbstractFlashcardViewer.isDestroyed) { super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl) @@ -2010,7 +2067,12 @@ abstract class AbstractFlashcardViewer : } } - override fun onScrollChanged(horiz: Int, vert: Int, oldHoriz: Int, oldVert: Int) { + override fun onScrollChanged( + horiz: Int, + vert: Int, + oldHoriz: Int, + oldVert: Int, + ) { super.onScrollChanged(horiz, vert, oldHoriz, oldVert) if (abs(horiz - oldHoriz) > abs(vert - oldVert)) { mIsXScrolling = true @@ -2035,7 +2097,7 @@ abstract class AbstractFlashcardViewer : scrollX: Int, scrollY: Int, clampedX: Boolean, - clampedY: Boolean + clampedY: Boolean, ) { if (clampedX) { val scrollParent = findScrollParent(this) @@ -2064,7 +2126,7 @@ abstract class AbstractFlashcardViewer : e1: MotionEvent?, e2: MotionEvent, velocityX: Float, - velocityY: Float + velocityY: Float, ): Boolean { Timber.d("onFling") @@ -2089,7 +2151,7 @@ abstract class AbstractFlashcardViewer : velocityY, mIsSelecting, mIsXScrolling, - mIsYScrolling + mIsYScrolling, ) } catch (e: Exception) { Timber.e(e, "onFling Exception") @@ -2177,9 +2239,10 @@ abstract class AbstractFlashcardViewer : Timber.d("Initializing shake detector") if (mGestureProcessor.isBound(Gesture.SHAKE)) { val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager - shakeDetector = ShakeDetector(this).apply { - start(sensorManager, SensorManager.SENSOR_DELAY_UI) - } + shakeDetector = + ShakeDetector(this).apply { + start(sensorManager, SensorManager.SENSOR_DELAY_UI) + } } } @@ -2249,12 +2312,13 @@ abstract class AbstractFlashcardViewer : return@setOnTouchListener true } val cardWebView = webViewAsView as WebView - val result: HitTestResult = try { - cardWebView.hitTestResult - } catch (e: Exception) { - Timber.w(e, "Cannot obtain HitTest result") - return@setOnTouchListener true - } + val result: HitTestResult = + try { + cardWebView.hitTestResult + } catch (e: Exception) { + Timber.w(e, "Cannot obtain HitTest result") + return@setOnTouchListener true + } if (isLinkClick(result)) { Timber.v("Detected link click - ignoring gesture dispatch") return@setOnTouchListener true @@ -2273,7 +2337,7 @@ abstract class AbstractFlashcardViewer : return ( type == HitTestResult.SRC_ANCHOR_TYPE || type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE - ) + ) } } @@ -2294,14 +2358,15 @@ abstract class AbstractFlashcardViewer : open var currentCard: Card? = null set(card) { field = card - mCardSoundConfig = if (card == null) { - null - } else { - if (mCardSoundConfig?.appliesTo(card) == true) { - return + mCardSoundConfig = + if (card == null) { + null + } else { + if (mCardSoundConfig?.appliesTo(card) == true) { + return + } + create(getColUnsafe, card) } - create(getColUnsafe, card) - } } /** Refreshes the WebView after a crash */ @@ -2345,6 +2410,7 @@ abstract class AbstractFlashcardViewer : const val ANSWER_ORDINAL_2 = 6 const val ANSWER_ORDINAL_3 = 7 const val ANSWER_ORDINAL_4 = 8 + fun getSignalFromUrl(url: String): Int { when (url) { "signal:typefocus" -> return TYPE_FOCUS @@ -2366,20 +2432,26 @@ abstract class AbstractFlashcardViewer : protected inner class CardViewerWebClient internal constructor( private val loader: WebViewAssetLoader?, - private val onPageFinishedCallback: OnPageFinishedCallback? = null + private val onPageFinishedCallback: OnPageFinishedCallback? = null, ) : WebViewClient() { private var pageFinishedFired = true private val pageRenderStopwatch = Stopwatch.init("page render") @TargetApi(Build.VERSION_CODES.N) - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest, + ): Boolean { val url = request.url.toString() Timber.d("Obtained URL from card: '%s'", url) return filterUrl(url) } // required for lower APIs (I think) - override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { + override fun shouldInterceptRequest( + view: WebView, + url: String, + ): WebResourceResponse? { // response is null if nothing required if (isLoadedFromProtocolRelativeUrl(url)) { mMissingImageHandler.processInefficientImage { displayMediaUpgradeRequiredSnackbar() } @@ -2387,7 +2459,11 @@ abstract class AbstractFlashcardViewer : return null } - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + override fun onPageStarted( + view: WebView?, + url: String?, + favicon: Bitmap?, + ) { pageRenderStopwatch.reset() pageFinishedFired = false } @@ -2395,7 +2471,7 @@ abstract class AbstractFlashcardViewer : @TargetApi(Build.VERSION_CODES.N) override fun shouldInterceptRequest( view: WebView, - request: WebResourceRequest + request: WebResourceRequest, ): WebResourceResponse? { val url = request.url val result = loader!!.shouldInterceptRequest(url) @@ -2420,12 +2496,12 @@ abstract class AbstractFlashcardViewer : override fun onReceivedError( view: WebView, request: WebResourceRequest, - error: WebResourceError + error: WebResourceError, ) { super.onReceivedError(view, request, error) mMissingImageHandler.processFailure(request) { filename: String? -> displayCouldNotFindMediaSnackbar( - filename + filename, ) } } @@ -2433,17 +2509,20 @@ abstract class AbstractFlashcardViewer : override fun onReceivedHttpError( view: WebView, request: WebResourceRequest, - errorResponse: WebResourceResponse + errorResponse: WebResourceResponse, ) { super.onReceivedHttpError(view, request, errorResponse) mMissingImageHandler.processFailure(request) { filename: String? -> displayCouldNotFindMediaSnackbar( - filename + filename, ) } } - override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + override fun shouldOverrideUrlLoading( + view: WebView, + url: String, + ): Boolean { return filterUrl(url) } @@ -2485,7 +2564,7 @@ abstract class AbstractFlashcardViewer : showThemedToast( this@AbstractFlashcardViewer, getString(R.string.ankidroid_turn_on_fullscreen_options_menu), - true + true, ) } return true @@ -2499,7 +2578,7 @@ abstract class AbstractFlashcardViewer : showThemedToast( this@AbstractFlashcardViewer, getString(R.string.ankidroid_turn_on_fullscreen_nav_drawer), - true + true, ) } return true @@ -2579,28 +2658,29 @@ abstract class AbstractFlashcardViewer : if (intent != null) { if (packageManager.resolveActivityCompat( intent, - ResolveInfoFlagsCompat.EMPTY + ResolveInfoFlagsCompat.EMPTY, ) == null ) { val packageName = intent.getPackage() if (packageName == null) { Timber.d( "Not using resolved intent uri because not available: %s", - intent + intent, ) intent = null } else { Timber.d( "Resolving intent uri to market uri because not available: %s", - intent - ) - intent = Intent( - Intent.ACTION_VIEW, - Uri.parse("market://details?id=$packageName") + intent, ) + intent = + Intent( + Intent.ACTION_VIEW, + Uri.parse("market://details?id=$packageName"), + ) if (packageManager.resolveActivityCompat( intent, - ResolveInfoFlagsCompat.EMPTY + ResolveInfoFlagsCompat.EMPTY, ) == null ) { intent = null @@ -2636,15 +2716,17 @@ abstract class AbstractFlashcardViewer : @NeedsTest("14221: 'playsound' should play the sound from the start") @BlocksSchemaUpgrade("handle TTS tags") private suspend fun controlSound(url: String) { - val filename = when (val tag = currentCard?.let { getAvTag(it, url) }) { - is SoundOrVideoTag -> tag.filename - // not currently supported - is TTSTag -> null - else -> null - } - val replacedUrl = filename?.let { - Sound.getSoundPath(mBaseUrl!!, it) - } ?: return + val filename = + when (val tag = currentCard?.let { getAvTag(it, url) }) { + is SoundOrVideoTag -> tag.filename + // not currently supported + is TTSTag -> null + else -> null + } + val replacedUrl = + filename?.let { + Sound.getSoundPath(mBaseUrl!!, it) + } ?: return mSoundPlayer.playSound(replacedUrl, null, soundErrorListener) } @@ -2658,14 +2740,17 @@ abstract class AbstractFlashcardViewer : showThemedToast( this@AbstractFlashcardViewer, getString(R.string.card_viewer_url_decode_error), - true + true, ) } return "" } // Run any post-load events in javascript that rely on the window being completely loaded. - override fun onPageFinished(view: WebView, url: String) { + override fun onPageFinished( + view: WebView, + url: String, + ) { if (pageFinishedFired) { return } @@ -2678,7 +2763,10 @@ abstract class AbstractFlashcardViewer : } @TargetApi(Build.VERSION_CODES.O) - override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean { + override fun onRenderProcessGone( + view: WebView, + detail: RenderProcessGoneDetail, + ): Boolean { return mOnRenderProcessGoneDelegate.onRenderProcessGone(view, detail) } } @@ -2700,8 +2788,9 @@ abstract class AbstractFlashcardViewer : fun handleUrlFromJavascript(url: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // WebViewCompat recommended here, but I'll avoid the dependency as it's test code - val c = webView?.webViewClient as? CardViewerWebClient? - ?: throw IllegalStateException("Couldn't obtain WebView - maybe it wasn't created yet") + val c = + webView?.webViewClient as? CardViewerWebClient? + ?: throw IllegalStateException("Couldn't obtain WebView - maybe it wasn't created yet") c.filterUrl(url) } else { throw IllegalStateException("Can't get WebViewClient due to Android API") @@ -2714,8 +2803,9 @@ abstract class AbstractFlashcardViewer : internal fun showTagsDialog() { val tags = ArrayList(getColUnsafe.tags.all()) val selTags = ArrayList(currentCard!!.note().tags) - val dialog = mTagsDialogFactory!!.newTagsDialog() - .withArguments(TagsDialog.DialogType.EDIT_TAGS, selTags, tags) + val dialog = + mTagsDialogFactory!!.newTagsDialog() + .withArguments(TagsDialog.DialogType.EDIT_TAGS, selTags, tags) showDialogFragment(dialog) } @@ -2723,7 +2813,7 @@ abstract class AbstractFlashcardViewer : override fun onSelectedTags( selectedTags: List, indeterminateTags: List, - stateFilter: CardStateFilter + stateFilter: CardStateFilter, ) { if (currentCard!!.note().tags != selectedTags) { val tagString = selectedTags.joinToString(" ") @@ -2739,7 +2829,10 @@ abstract class AbstractFlashcardViewer : return AnkiDroidJsAPI(this) } - override fun opExecuted(changes: OpChanges, handler: Any?) { + override fun opExecuted( + changes: OpChanges, + handler: Any?, + ) { if (handler === this) return refreshRequired = ViewerRefresh.updateState(refreshRequired, changes) refreshIfRequired() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt index cdf4af38142d..8be458721be0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt @@ -26,7 +26,6 @@ import androidx.core.view.ActionProvider * @see ActionProvider */ abstract class ActionProviderCompat(context: Context) : ActionProvider(context) { - @Deprecated("Override onCreateActionView(MenuItem)") override fun onCreateActionView(): View { // The previous code returned null from this method but updates to the core-ktx library diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AndroidTtsPlayer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AndroidTtsPlayer.kt index 399a06213e8e..203310558075 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AndroidTtsPlayer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AndroidTtsPlayer.kt @@ -42,7 +42,6 @@ import kotlin.coroutines.resume class AndroidTtsPlayer(private val context: Context, private val voices: List) : TtsPlayer(), DefaultLifecycleObserver { - private lateinit var scope: CoroutineScope // this can be null in the case that TTS failed to load @@ -52,21 +51,28 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List = Channel() + suspend fun init(scope: CoroutineScope) { this.scope = scope - this.tts = TtsVoices.createTts(context)?.apply { - setOnUtteranceProgressListener(object : UtteranceProgressListenerCompat() { - override fun onStart(utteranceId: String?) { } - - override fun onDone(utteranceId: String?) { - scope.launch(Dispatchers.IO) { ttsCompletedChannel.send(TtsCompletionStatus.success()) } - } - - override fun onError(utteranceId: String?, errorCode: Int) { - scope.launch(Dispatchers.IO) { ttsCompletedChannel.send(AndroidTtsError.failure(errorCode)) } - } - }) - } + this.tts = + TtsVoices.createTts(context)?.apply { + setOnUtteranceProgressListener( + object : UtteranceProgressListenerCompat() { + override fun onStart(utteranceId: String?) { } + + override fun onDone(utteranceId: String?) { + scope.launch(Dispatchers.IO) { ttsCompletedChannel.send(TtsCompletionStatus.success()) } + } + + override fun onError( + utteranceId: String?, + errorCode: Int, + ) { + scope.launch(Dispatchers.IO) { ttsCompletedChannel.send(AndroidTtsError.failure(errorCode)) } + } + }, + ) + } } override fun onCreate(owner: LifecycleOwner) { @@ -88,7 +94,7 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List - val tts = tts?.also { - it.voice = voice.voice - if (it.setSpeechRate(tag.speed) == ERROR) { - return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.failure(TtsErrorCode.APP_SPEECH_RATE_FAILED)) - } - // if it's already playing: stop it - it.stopPlaying() - } ?: return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.failure(TtsErrorCode.APP_TTS_INIT_FAILED)) + val tts = + tts?.also { + it.voice = voice.voice + if (it.setSpeechRate(tag.speed) == ERROR) { + return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.failure(TtsErrorCode.APP_SPEECH_RATE_FAILED)) + } + // if it's already playing: stop it + it.stopPlaying() + } ?: return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.failure(TtsErrorCode.APP_TTS_INIT_FAILED)) Timber.d("tts text '%s' to be played for locale (%s)", tag.fieldText, tag.lang) tts.speak(tag.fieldText, TextToSpeech.QUEUE_FLUSH, bundleFlyweight, "stringId") @@ -127,7 +137,7 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List= Build.VERSION_CODES.O_MR1) { @@ -107,7 +108,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { super.onResume() UsageAnalytics.sendAnalyticsScreenView(this) (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).cancel( - SIMPLE_NOTIFICATION_ID + SIMPLE_NOTIFICATION_ID, ) // Show any pending dialogs which were stored persistently dialogHandler.executeMessage() @@ -170,14 +171,20 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { super.setContentView(view) } - override fun setContentView(view: View, params: ViewGroup.LayoutParams) { + override fun setContentView( + view: View, + params: ViewGroup.LayoutParams, + ) { if (animationDisabled()) { view.clearAnimation() } super.setContentView(view, params) } - override fun addContentView(view: View, params: ViewGroup.LayoutParams) { + override fun addContentView( + view: View, + params: ViewGroup.LayoutParams, + ) { if (animationDisabled()) { view.clearAnimation() } @@ -196,7 +203,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { fun startActivityWithAnimation( intent: Intent, - animation: Direction + animation: Direction, ) { enableIntentAnimation(intent) super.startActivity(intent) @@ -206,12 +213,12 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { private fun launchActivityForResult( intent: Intent?, launcher: ActivityResultLauncher, - animation: Direction? + animation: Direction?, ) { try { launcher.launch( intent, - ActivityTransitionAnimation.getAnimationOptions(this, animation) + ActivityTransitionAnimation.getAnimationOptions(this, animation), ) } catch (e: ActivityNotFoundException) { Timber.w(e) @@ -234,7 +241,10 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { view.clearAnimation() } - protected fun enableViewAnimation(view: View, animation: Animation?) { + protected fun enableViewAnimation( + view: View, + animation: Animation?, + ) { if (animationDisabled()) { disableViewAnimation(view) } else { @@ -276,7 +286,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { // Open collection asynchronously if it hasn't already been opened showProgressBar() CollectionLoader.load( - this + this, ) { col: Collection? -> if (col != null) { Timber.d("Asynchronously calling onCollectionLoaded") @@ -325,28 +335,30 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { showThemedToast( this, resources.getString(R.string.no_browser_notification) + url, - false + false, ) return } val toolbarColor = MaterialColors.getColor(this, R.attr.appBarColor, 0) val navBarColor = MaterialColors.getColor(this, R.attr.customTabNavBarColor, 0) - val colorSchemeParams = CustomTabColorSchemeParams.Builder() - .setToolbarColor(toolbarColor) - .setNavigationBarColor(navBarColor) - .build() - val builder = CustomTabsIntent.Builder(customTabActivityHelper.session) - .setShowTitle(true) - .setStartAnimations(this, R.anim.slide_right_in, R.anim.slide_left_out) - .setExitAnimations(this, R.anim.slide_left_in, R.anim.slide_right_out) - .setCloseButtonIcon( - BitmapFactory.decodeResource( - this.resources, - R.drawable.ic_back_arrow_custom_tab + val colorSchemeParams = + CustomTabColorSchemeParams.Builder() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navBarColor) + .build() + val builder = + CustomTabsIntent.Builder(customTabActivityHelper.session) + .setShowTitle(true) + .setStartAnimations(this, R.anim.slide_right_in, R.anim.slide_left_out) + .setExitAnimations(this, R.anim.slide_left_in, R.anim.slide_right_out) + .setCloseButtonIcon( + BitmapFactory.decodeResource( + this.resources, + R.drawable.ic_back_arrow_custom_tab, + ), ) - ) - .setColorScheme(customTabsColorScheme) - .setDefaultColorSchemeParams(colorSchemeParams) + .setColorScheme(customTabsColorScheme) + .setDefaultColorSchemeParams(colorSchemeParams) val customTabsIntent = builder.build() CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent) CustomTabActivityHelper.openCustomTab(this, customTabsIntent, url, CustomTabsFallback()) @@ -356,18 +368,21 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { openUrl(Uri.parse(urlString)) } - fun openUrl(@StringRes url: Int) { + fun openUrl( + @StringRes url: Int, + ) { openUrl(getString(url)) } private val customTabsColorScheme: Int - get() = if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) { - COLOR_SCHEME_SYSTEM - } else if (Themes.currentTheme.isNightMode) { - COLOR_SCHEME_DARK - } else { - COLOR_SCHEME_LIGHT - } + get() = + if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) { + COLOR_SCHEME_SYSTEM + } else if (Themes.currentTheme.isNightMode) { + COLOR_SCHEME_DARK + } else { + COLOR_SCHEME_LIGHT + } /** * Global method to show dialog fragment including adding it to back stack Note: DO NOT call this from an async @@ -401,7 +416,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { */ fun showAsyncDialogFragment( newFragment: AsyncDialogFragment, - channel: Channel + channel: Channel, ) { try { showDialogFragment(newFragment) @@ -428,7 +443,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { open fun showSimpleMessageDialog( message: String?, title: String = "", - reload: Boolean = false + reload: Boolean = false, ) { val newFragment: AsyncDialogFragment = SimpleMessageDialog.newInstance(title, message, reload) @@ -438,31 +453,33 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { fun showSimpleNotification( title: String, message: String?, - channel: Channel + channel: Channel, ) { val prefs = this.sharedPrefs() // Show a notification unless all notifications have been totally disabled if (prefs.getString(MINIMUM_CARDS_DUE_FOR_NOTIFICATION, "0")!! - .toInt() <= Preferences.PENDING_NOTIFICATIONS_ONLY + .toInt() <= Preferences.PENDING_NOTIFICATIONS_ONLY ) { // Use the title as the ticker unless the title is simply "AnkiDroid" - val ticker: String? = if (title == resources.getString(R.string.app_name)) { - message - } else { - title - } + val ticker: String? = + if (title == resources.getString(R.string.app_name)) { + message + } else { + title + } // Build basic notification - val builder = NotificationCompat.Builder( - this, - channel.id - ) - .setSmallIcon(R.drawable.ic_star_notify) - .setContentTitle(title) - .setContentText(message) - .setColor(this.getColor(R.color.material_light_blue_500)) - .setStyle(NotificationCompat.BigTextStyle().bigText(message)) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setTicker(ticker) + val builder = + NotificationCompat.Builder( + this, + channel.id, + ) + .setSmallIcon(R.drawable.ic_star_notify) + .setContentTitle(title) + .setContentText(message) + .setColor(this.getColor(R.color.material_light_blue_500)) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setTicker(ticker) // Enable vibrate and blink if set in preferences if (prefs.getBoolean("widgetVibrate", false)) { builder.setVibrate(longArrayOf(1000, 1000, 1000)) @@ -473,13 +490,14 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { // Creates an explicit intent for an Activity in your app val resultIntent = Intent(this, DeckPicker::class.java) resultIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - val resultPendingIntent = PendingIntentCompat.getActivity( - this, - 0, - resultIntent, - PendingIntent.FLAG_UPDATE_CURRENT, - false - ) + val resultPendingIntent = + PendingIntentCompat.getActivity( + this, + 0, + resultIntent, + PendingIntent.FLAG_UPDATE_CURRENT, + false, + ) builder.setContentIntent(resultPendingIntent) val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager // mId allows you to update the notification later on. @@ -501,7 +519,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { fun dismissAllDialogFragments() { supportFragmentManager.popBackStack( DIALOG_FRAGMENT_TAG, - FragmentManager.POP_BACK_STACK_INCLUSIVE + FragmentManager.POP_BACK_STACK_INCLUSIVE, ) } @@ -511,9 +529,10 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { * @throws IllegalStateException if the bar could not be enabled */ protected fun enableToolbar(): ActionBar { - val toolbar = findViewById(R.id.toolbar) - ?: // likely missing "" - throw IllegalStateException("Unable to find toolbar") + val toolbar = + findViewById(R.id.toolbar) + ?: // likely missing "" + throw IllegalStateException("Unable to find toolbar") setSupportActionBar(toolbar) return supportActionBar!! } @@ -525,9 +544,10 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { * @throws IllegalStateException if the bar could not be enabled */ protected fun enableToolbar(view: View): ActionBar { - val toolbar = view.findViewById(R.id.toolbar) - ?: // likely missing "" - throw IllegalStateException("Unable to find toolbar: $view") + val toolbar = + view.findViewById(R.id.toolbar) + ?: // likely missing "" + throw IllegalStateException("Unable to find toolbar: $view") setSupportActionBar(toolbar) return supportActionBar!! } @@ -535,7 +555,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { protected fun showedActivityFailedScreen(savedInstanceState: Bundle?) = showedActivityFailedScreen( savedInstanceState = savedInstanceState, - activitySuperOnCreate = { state -> super.onCreate(state) } + activitySuperOnCreate = { state -> super.onCreate(state) }, ) companion object { @@ -544,11 +564,17 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { /** Extra key to set the finish animation of an activity */ const val FINISH_ANIMATION_EXTRA = "finishAnimation" - fun showDialogFragment(activity: AnkiActivity, newFragment: DialogFragment) { + fun showDialogFragment( + activity: AnkiActivity, + newFragment: DialogFragment, + ) { showDialogFragment(activity.supportFragmentManager, newFragment) } - fun showDialogFragment(manager: FragmentManager, newFragment: DialogFragment) { + fun showDialogFragment( + manager: FragmentManager, + newFragment: DialogFragment, + ) { // DialogFragment.show() will take care of adding the fragment // in a transaction. We also want to remove any currently showing // dialog, so make our own transaction and take care of that here. diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt index fd2b92543b62..6781c1a26822 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt @@ -149,12 +149,12 @@ open class AnkiDroidApp : Application() { this, preferences.getBoolean( getString(R.string.card_browser_external_context_menu_key), - false - ) + false, + ), ) AnkiCardContextMenu.ensureConsistentStateWithPreferenceStatus( this, - preferences.getBoolean(getString(R.string.anki_card_external_context_menu_key), true) + preferences.getBoolean(getString(R.string.anki_card_external_context_menu_key), true), ) CompatHelper.compat.setupNotificationChannel(applicationContext) @@ -189,41 +189,49 @@ open class AnkiDroidApp : Application() { // Register for notifications mNotifications.observeForever { NotificationService.triggerNotificationFor(this) } - registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - Timber.i("${activity::class.simpleName}::onCreate") - (activity as? FragmentActivity) - ?.supportFragmentManager - ?.registerFragmentLifecycleCallbacks( - FragmentLifecycleLogger(activity), - true - ) - } + registerActivityLifecycleCallbacks( + object : ActivityLifecycleCallbacks { + override fun onActivityCreated( + activity: Activity, + savedInstanceState: Bundle?, + ) { + Timber.i("${activity::class.simpleName}::onCreate") + (activity as? FragmentActivity) + ?.supportFragmentManager + ?.registerFragmentLifecycleCallbacks( + FragmentLifecycleLogger(activity), + true, + ) + } - override fun onActivityStarted(activity: Activity) { - Timber.i("${activity::class.simpleName}::onStart") - } + override fun onActivityStarted(activity: Activity) { + Timber.i("${activity::class.simpleName}::onStart") + } - override fun onActivityResumed(activity: Activity) { - Timber.i("${activity::class.simpleName}::onResume") - } + override fun onActivityResumed(activity: Activity) { + Timber.i("${activity::class.simpleName}::onResume") + } - override fun onActivityPaused(activity: Activity) { - Timber.i("${activity::class.simpleName}::onPause") - } + override fun onActivityPaused(activity: Activity) { + Timber.i("${activity::class.simpleName}::onPause") + } - override fun onActivityStopped(activity: Activity) { - Timber.i("${activity::class.simpleName}::onStop") - } + override fun onActivityStopped(activity: Activity) { + Timber.i("${activity::class.simpleName}::onStop") + } - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { - Timber.i("${activity::class.simpleName}::onSaveInstanceState") - } + override fun onActivitySaveInstanceState( + activity: Activity, + outState: Bundle, + ) { + Timber.i("${activity::class.simpleName}::onSaveInstanceState") + } - override fun onActivityDestroyed(activity: Activity) { - Timber.i("${activity::class.simpleName}::onDestroy") - } - }) + override fun onActivityDestroyed(activity: Activity) { + Timber.i("${activity::class.simpleName}::onDestroy") + } + }, + ) activityAgnosticDialogs = ActivityAgnosticDialogs.register(this) TtsVoices.launchBuildLocalesJob() @@ -242,7 +250,7 @@ open class AnkiDroidApp : Application() { "android:%s:%s:%s", BuildConfig.VERSION_NAME, Build.VERSION.RELEASE, - model + model, ) } @@ -315,7 +323,12 @@ open class AnkiDroidApp : Application() { } // ---- END copied from Timber.DebugTree because DebugTree.getTag() is package private ---- - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + override fun log( + priority: Int, + tag: String?, + message: String, + t: Throwable?, + ) { when (priority) { Log.VERBOSE, Log.DEBUG -> {} Log.INFO -> Log.i(TAG, message, t) @@ -332,7 +345,6 @@ open class AnkiDroidApp : Application() { } companion object { - /** * [CoroutineScope] tied to the [Application], allowing executing of tasks which should * execute as long as the app is running @@ -445,6 +457,7 @@ open class AnkiDroidApp : Application() { return Intent(Intent.ACTION_VIEW, parsed) } // TODO actually this can be done by translating "link_help" string for each language when the App is // properly translated + /** * Get the url for the feedback page * @return @@ -489,7 +502,12 @@ open class AnkiDroidApp : Application() { } class RobolectricDebugTree : DebugTree() { - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + override fun log( + priority: Int, + tag: String?, + message: String, + t: Throwable?, + ) { // This is noisy in test environments when (tag) { "Backend\$checkMainThreadOp" -> return @@ -501,12 +519,12 @@ open class AnkiDroidApp : Application() { } private class FragmentLifecycleLogger( - private val activity: Activity + private val activity: Activity, ) : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentAttached( fm: FragmentManager, f: Fragment, - context: Context + context: Context, ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onAttach") } @@ -514,7 +532,7 @@ open class AnkiDroidApp : Application() { override fun onFragmentCreated( fm: FragmentManager, f: Fragment, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onCreate") } @@ -523,44 +541,65 @@ open class AnkiDroidApp : Application() { fm: FragmentManager, f: Fragment, v: View, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onViewCreated") } - override fun onFragmentStarted(fm: FragmentManager, f: Fragment) { + override fun onFragmentStarted( + fm: FragmentManager, + f: Fragment, + ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onStart") } - override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + override fun onFragmentResumed( + fm: FragmentManager, + f: Fragment, + ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onResume") } - override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { + override fun onFragmentPaused( + fm: FragmentManager, + f: Fragment, + ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onPause") } - override fun onFragmentStopped(fm: FragmentManager, f: Fragment) { + override fun onFragmentStopped( + fm: FragmentManager, + f: Fragment, + ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onStop") } override fun onFragmentSaveInstanceState( fm: FragmentManager, f: Fragment, - outState: Bundle + outState: Bundle, ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onSaveInstanceState") } - override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) { + override fun onFragmentViewDestroyed( + fm: FragmentManager, + f: Fragment, + ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onViewDestroyed") } - override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) { + override fun onFragmentDestroyed( + fm: FragmentManager, + f: Fragment, + ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onDestroy") } - override fun onFragmentDetached(fm: FragmentManager, f: Fragment) { + override fun onFragmentDetached( + fm: FragmentManager, + f: Fragment, + ) { Timber.i("${activity::class.simpleName}::${f::class.simpleName}::onDetach") } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index 38b0f88a8ed0..e94aae4bbf6e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -63,19 +63,31 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { // Text to speech private val mTalker = JavaScriptTTS() - open fun convertToByteArray(apiContract: ApiContract, boolean: Boolean): ByteArray { + open fun convertToByteArray( + apiContract: ApiContract, + boolean: Boolean, + ): ByteArray { return ApiResult(apiContract.isValid, boolean.toString()).toString().toByteArray() } - open fun convertToByteArray(apiContract: ApiContract, int: Int): ByteArray { + open fun convertToByteArray( + apiContract: ApiContract, + int: Int, + ): ByteArray { return ApiResult(apiContract.isValid, int.toString()).toString().toByteArray() } - open fun convertToByteArray(apiContract: ApiContract, long: Long): ByteArray { + open fun convertToByteArray( + apiContract: ApiContract, + long: Long, + ): ByteArray { return ApiResult(apiContract.isValid, long.toString()).toString().toByteArray() } - open fun convertToByteArray(apiContract: ApiContract, string: String): ByteArray { + open fun convertToByteArray( + apiContract: ApiContract, + string: String, + ): ByteArray { return ApiResult(apiContract.isValid, string).toString().toByteArray() } @@ -109,7 +121,10 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * * show developer contact if js api used in card is deprecated */ - private fun showDeveloperContact(errorCode: Int, apiDevContact: String) { + private fun showDeveloperContact( + errorCode: Int, + apiDevContact: String, + ) { val errorMsg: String = context.getString(R.string.anki_js_error_code, errorCode) val snackbarMsg: String = context.getString(R.string.api_version_developer_contact, apiDevContact, errorMsg) @@ -124,7 +139,10 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { /** * Supplied api version must be equal to current api version to call mark card, toggle flag functions etc. */ - private fun requireApiVersion(apiVer: String, apiDevContact: String): Boolean { + private fun requireApiVersion( + apiVer: String, + apiDevContact: String, + ): Boolean { try { if (apiDevContact.isEmpty() || apiVer.isEmpty()) { activity.runOnUiThread { @@ -136,10 +154,10 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { val versionSupplied = Version.valueOf(apiVer) /* - * if api major version equals to supplied major version then return true and also check for minor version and patch version - * show toast for update and contact developer if need updates - * otherwise return false - */ + * if api major version equals to supplied major version then return true and also check for minor version and patch version + * show toast for update and contact developer if need updates + * otherwise return false + */ return when { versionSupplied == versionCurrent -> { true @@ -171,7 +189,11 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * @param isReviewer * @return */ - open suspend fun handleJsApiRequest(methodName: String, bytes: ByteArray, isReviewer: Boolean) = withContext(Dispatchers.Main) { + open suspend fun handleJsApiRequest( + methodName: String, + bytes: ByteArray, + isReviewer: Boolean, + ) = withContext(Dispatchers.Main) { // the method will call to set the card supplied data and is valid version for each api request val apiContract = parseJsApiContract(bytes)!! // if api not init or is api not called from reviewer then return default -1 @@ -200,7 +222,13 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } convertToByteArray(apiContract, activity.executeCommand(flagCommands[apiParams]!!)) } - "markCard" -> processAction({ activity.executeCommand(ViewerCommand.MARK) }, apiContract, ankiJsErrorCodeMarkCard, ::convertToByteArray) + "markCard" -> + processAction( + { activity.executeCommand(ViewerCommand.MARK) }, + apiContract, + ankiJsErrorCodeMarkCard, + ::convertToByteArray, + ) "buryCard" -> processAction(activity::buryCard, apiContract, ankiJsErrorCodeBuryCard, ::convertToByteArray) "buryNote" -> processAction(activity::buryNote, apiContract, ankiJsErrorCodeBuryNote, ::convertToByteArray) "suspendCard" -> processAction(activity::suspendCard, apiContract, ankiJsErrorCodeSuspendCard, ::convertToByteArray) @@ -261,10 +289,11 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } "ttsStop" -> convertToByteArray(apiContract, mTalker.stop()) "searchCard" -> { - val intent = Intent(context, CardBrowser::class.java).apply { - putExtra("currentCard", currentCard.id) - putExtra("search_query", apiParams) - } + val intent = + Intent(context, CardBrowser::class.java).apply { + putExtra("currentCard", currentCard.id) + putExtra("search_query", apiParams) + } activity.startActivity(intent) convertToByteArray(apiContract, true) } @@ -296,7 +325,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { action: () -> Boolean, apiContract: ApiContract, errorCode: Int, - conversion: (ApiContract, Boolean) -> ByteArray + conversion: (ApiContract, Boolean) -> ByteArray, ): ByteArray { val status = action() if (!status) { @@ -305,44 +334,46 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { return conversion(apiContract, status) } - private suspend fun ankiSearchCardWithCallback(apiContract: ApiContract): ByteArray = withContext(Dispatchers.Main) { - val cards = try { - searchForCards(apiContract.cardSuppliedData, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) - } catch (exc: Exception) { - activity.webView!!.evaluateJavascript( - "console.log('${context.getString(R.string.search_card_js_api_no_results)}')", - null - ) - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSearchCard, apiContract.cardSuppliedDeveloperContact) - return@withContext convertToByteArray(apiContract, false) - } - val searchResult: MutableList = ArrayList() - for (s in cards) { - val jsonObject = JSONObject() - val fieldsData = s.card.note().fields - val fieldsName = s.card.model().fieldsNames + private suspend fun ankiSearchCardWithCallback(apiContract: ApiContract): ByteArray = + withContext(Dispatchers.Main) { + val cards = + try { + searchForCards(apiContract.cardSuppliedData, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) + } catch (exc: Exception) { + activity.webView!!.evaluateJavascript( + "console.log('${context.getString(R.string.search_card_js_api_no_results)}')", + null, + ) + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSearchCard, apiContract.cardSuppliedDeveloperContact) + return@withContext convertToByteArray(apiContract, false) + } + val searchResult: MutableList = ArrayList() + for (s in cards) { + val jsonObject = JSONObject() + val fieldsData = s.card.note().fields + val fieldsName = s.card.model().fieldsNames - val noteId = s.card.note().id - val cardId = s.card.id - jsonObject.put("cardId", cardId) - jsonObject.put("noteId", noteId) + val noteId = s.card.note().id + val cardId = s.card.id + jsonObject.put("cardId", cardId) + jsonObject.put("noteId", noteId) - val jsonFieldObject = JSONObject() - fieldsName.zip(fieldsData).forEach { pair -> - jsonFieldObject.put(pair.component1(), pair.component2()) - } - jsonObject.put("fieldsData", jsonFieldObject) + val jsonFieldObject = JSONObject() + fieldsName.zip(fieldsData).forEach { pair -> + jsonFieldObject.put(pair.component1(), pair.component2()) + } + jsonObject.put("fieldsData", jsonFieldObject) - searchResult.add(jsonObject.toString()) - } + searchResult.add(jsonObject.toString()) + } - // quote result to prevent JSON injection attack - val jsonEncodedString = JSONObject.quote(searchResult.toString()) - activity.runOnUiThread { - activity.webView!!.evaluateJavascript("ankiSearchCard($jsonEncodedString)", null) + // quote result to prevent JSON injection attack + val jsonEncodedString = JSONObject.quote(searchResult.toString()) + activity.runOnUiThread { + activity.webView!!.evaluateJavascript("ankiSearchCard($jsonEncodedString)", null) + } + convertToByteArray(apiContract, true) } - convertToByteArray(apiContract, true) - } open class CardDataForJsApi { var newCardCount = "" diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt index 1f32a17486e9..ec5a618f33ba 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt @@ -38,14 +38,15 @@ object AnkiDroidJsAPIConstants { const val sCurrentJsApiVersion = "0.0.2" const val sMinimumJsApiVersion = "0.0.2" - val flagCommands = mapOf( - "none" to ViewerCommand.UNSET_FLAG, - "red" to ViewerCommand.TOGGLE_FLAG_RED, - "orange" to ViewerCommand.TOGGLE_FLAG_ORANGE, - "green" to ViewerCommand.TOGGLE_FLAG_GREEN, - "blue" to ViewerCommand.TOGGLE_FLAG_BLUE, - "pink" to ViewerCommand.TOGGLE_FLAG_PINK, - "turquoise" to ViewerCommand.TOGGLE_FLAG_TURQUOISE, - "purple" to ViewerCommand.TOGGLE_FLAG_PURPLE - ) + val flagCommands = + mapOf( + "none" to ViewerCommand.UNSET_FLAG, + "red" to ViewerCommand.TOGGLE_FLAG_RED, + "orange" to ViewerCommand.TOGGLE_FLAG_ORANGE, + "green" to ViewerCommand.TOGGLE_FLAG_GREEN, + "blue" to ViewerCommand.TOGGLE_FLAG_BLUE, + "pink" to ViewerCommand.TOGGLE_FLAG_PINK, + "turquoise" to ViewerCommand.TOGGLE_FLAG_TURQUOISE, + "purple" to ViewerCommand.TOGGLE_FLAG_PURPLE, + ) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt index 114374f0b283..c8faaf50f6b5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt @@ -37,7 +37,7 @@ fun Activity.importColpkg(colpkgPath: String) where Activity : AnkiAc if (progress.hasImporting()) { text = progress.importing } - } + }, ) { CollectionManager.importColpkg(colpkgPath) } @@ -52,7 +52,7 @@ private suspend fun createBackup(force: Boolean) { createBackup( BackupManager.getBackupDirectoryFromCollection(this.path), force, - waitForCompletion = false + waitForCompletion = false, ) } // move this outside 'withCol' to avoid blocking diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt index 848e3d365b96..6360ed26077d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt @@ -42,14 +42,15 @@ suspend fun importJsonFileRaw(input: ByteArray): ByteArray { suspend fun FragmentActivity.importCsvRaw(input: ByteArray): ByteArray { return withContext(Dispatchers.Main) { - val output = withProgress( - extractProgress = { - if (progress.hasImporting()) { - text = progress.importing - } - }, - op = { withCol { importCsvRaw(input) } } - ) + val output = + withProgress( + extractProgress = { + if (progress.hasImporting()) { + text = progress.importing + } + }, + op = { withCol { importCsvRaw(input) } }, + ) val importResponse = ImportResponse.parseFrom(output) undoableOp { importResponse } output @@ -64,7 +65,8 @@ suspend fun FragmentActivity.importCsvRaw(input: ByteArray): ByteArray { * * NOTE: this should be used only with [android.webkit.WebView.evaluateJavascript]. */ -val hideShowButtonCss = """ +val hideShowButtonCss = + """ javascript:( function() { var hideShowButtonStyle = '.desktop-only { display: none !important; }'; @@ -73,7 +75,7 @@ val hideShowButtonCss = """ document.head.appendChild(newStyle); } )() -""".trimIndent() + """.trimIndent() /** * Calls the native [CardBrowser] to display the results of the search query constructed from the @@ -81,10 +83,11 @@ val hideShowButtonCss = """ */ suspend fun FragmentActivity.searchInBrowser(input: ByteArray): ByteArray { val searchString = withCol { buildSearchString(input) } - val starterIntent = Intent(this, CardBrowser::class.java).apply { - putExtra("search_query", searchString) - putExtra("all_decks", true) - } + val starterIntent = + Intent(this, CardBrowser::class.java).apply { + putExtra("search_query", searchString) + putExtra("all_decks", true) + } startActivity(starterIntent) return input } @@ -93,14 +96,14 @@ suspend fun AnkiActivity.exportApkg( apkgPath: String, withScheduling: Boolean, withMedia: Boolean, - limit: ExportLimit + limit: ExportLimit, ) { withProgress( extractProgress = { if (progress.hasExporting()) { text = getString(R.string.export_preparation_in_progress) } - } + }, ) { withCol { exportAnkiPackage(apkgPath, withScheduling, withMedia, limit) @@ -110,14 +113,14 @@ suspend fun AnkiActivity.exportApkg( suspend fun AnkiActivity.exportColpkg( colpkgPath: String, - withMedia: Boolean + withMedia: Boolean, ) { withProgress( extractProgress = { if (progress.hasExporting()) { text = getString(R.string.export_preparation_in_progress) } - } + }, ) { withCol { exportCollectionPackage(colpkgPath, withMedia, true) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt index 44ec5a11f6b2..3662e9e47bea 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt @@ -61,7 +61,10 @@ open class BackupManager { * @return Whether a thread was started to create a backup */ @Suppress("PMD.NPathComplexity") - fun performBackupInBackground(colPath: String, time: Time): Boolean { + fun performBackupInBackground( + colPath: String, + time: Time, + ): Boolean { val prefs = AnkiDroidApp.instance.baseContext.sharedPrefs() if (hasDisabledBackups(prefs)) { Timber.w("backups are disabled") @@ -78,7 +81,9 @@ open class BackupManager { // Abort backup if one was already made less than the allowed frequency val lastBackupDate = getLastBackupDate(colBackups) if (lastBackupDate != null && lastBackupDate.time + frequency * 60_000L > time.intTimeMS()) { - Timber.d("performBackup: No backup created. Last backup younger than the frequency allowed from preferences(currently set to $frequency minutes)") + Timber.d( + "performBackup: No backup created. Last backup younger than the frequency allowed from preferences(currently set to $frequency minutes)", + ) return false } val backupFilename = getNameForNewBackup(time) ?: return false @@ -113,7 +118,10 @@ open class BackupManager { return true } - fun isBackupUnnecessary(colFile: File, colBackups: Array): Boolean { + fun isBackupUnnecessary( + colFile: File, + colBackups: Array, + ): Boolean { val len = colBackups.size // If have no backups, then a backup is necessary @@ -136,21 +144,31 @@ open class BackupManager { } } - fun getBackupFile(colFile: File, backupFilename: String): File { + fun getBackupFile( + colFile: File, + backupFilename: String, + ): File { return File(getBackupDirectory(colFile.parentFile!!), backupFilename) } - fun performBackupInNewThread(colFile: File, backupFile: File) { + fun performBackupInNewThread( + colFile: File, + backupFile: File, + ) { Timber.i("Launching new thread to backup %s to %s", colFile.absolutePath, backupFile.path) - val thread: Thread = object : Thread() { - override fun run() { - performBackup(colFile, backupFile) + val thread: Thread = + object : Thread() { + override fun run() { + performBackup(colFile, backupFile) + } } - } thread.start() } - private fun performBackup(colFile: File, backupFile: File): Boolean { + private fun performBackup( + colFile: File, + backupFile: File, + ): Boolean { val colPath = colFile.absolutePath // Save collection file as zip archive return try { @@ -161,17 +179,18 @@ open class BackupManager { } // Delete old backup files if needed val prefs = AnkiDroidApp.instance.baseContext.sharedPrefs() - val backupLimits = BackupLimits.newBuilder() - .setDaily(prefs.getInt("daily_backups_to_keep", 8)) - .setWeekly(prefs.getInt("weekly_backups_to_keep", 8)) - .setMonthly(prefs.getInt("monthly_backups_to_keep", 8)) - .build() + val backupLimits = + BackupLimits.newBuilder() + .setDaily(prefs.getInt("daily_backups_to_keep", 8)) + .setWeekly(prefs.getInt("weekly_backups_to_keep", 8)) + .setMonthly(prefs.getInt("monthly_backups_to_keep", 8)) + .build() deleteColBackups(colPath, backupLimits) // set timestamp of file in order to avoid creating a new backup unless its changed if (!backupFile.setLastModified(colFile.lastModified())) { Timber.w( "performBackupInBackground() setLastModified() failed on file %s", - backupFile.name + backupFile.name, ) return false } @@ -187,7 +206,7 @@ open class BackupManager { return ( colFile.length() < MIN_BACKUP_COL_SIZE - ) + ) } fun hasFreeDiscSpace(colFile: File): Boolean { @@ -298,27 +317,33 @@ open class BackupManager { return false } - fun moveDatabaseToBrokenDirectory(colPath: String, moveConnectedFilesToo: Boolean, time: Time): Boolean { + fun moveDatabaseToBrokenDirectory( + colPath: String, + moveConnectedFilesToo: Boolean, + time: Time, + ): Boolean { val colFile = File(colPath) // move file val value: Date = time.genToday(utcOffset()) - var movedFilename = String.format( - Utils.ENGLISH_LOCALE, - colFile.name.replace(".anki2", "") + - "-corrupt-%tF.anki2", - value - ) + var movedFilename = + String.format( + Utils.ENGLISH_LOCALE, + colFile.name.replace(".anki2", "") + + "-corrupt-%tF.anki2", + value, + ) var movedFile = File(getBrokenDirectory(colFile.parentFile!!), movedFilename) var i = 1 while (movedFile.exists()) { - movedFile = File( - getBrokenDirectory(colFile.parentFile!!), - movedFilename.replace( - ".anki2", - "-$i.anki2" + movedFile = + File( + getBrokenDirectory(colFile.parentFile!!), + movedFilename.replace( + ".anki2", + "-$i.anki2", + ), ) - ) i++ } movedFilename = movedFile.name @@ -378,12 +403,13 @@ open class BackupManager { /** Changes in the file name pattern should be updated as well in * [getBackupTimeString] and [com.ichi2.anki.dialogs.DatabaseErrorDialog.onCreateDialog] */ val cal: Calendar = time.gregorianCalendar() - val backupFilename: String = try { - String.format(Utils.ENGLISH_LOCALE, "collection-%s.colpkg", legacyDateFormat.format(cal.time)) - } catch (e: UnknownFormatConversionException) { - Timber.w(e, "performBackup: error on creating backup filename") - return null - } + val backupFilename: String = + try { + String.format(Utils.ENGLISH_LOCALE, "collection-%s.colpkg", legacyDateFormat.format(cal.time)) + } catch (e: UnknownFormatConversionException) { + Timber.w(e, "performBackup: error on creating backup filename") + return null + } return backupFilename } @@ -393,14 +419,15 @@ open class BackupManager { */ fun getBackups(colFile: File): Array { val files = getBackupDirectory(colFile.parentFile!!).listFiles() ?: arrayOf() - val backups = files - .mapNotNull { file -> - getBackupTimeString(file.name)?.let { time -> - Pair(time, file) + val backups = + files + .mapNotNull { file -> + getBackupTimeString(file.name)?.let { time -> + Pair(time, file) + } } - } - .sortedBy { it.first } - .map { it.second } + .sortedBy { it.first } + .map { it.second } return backups.toTypedArray() } @@ -422,7 +449,7 @@ open class BackupManager { fun deleteColBackups( colPath: String, backupLimits: BackupLimits, - today: LocalDate = LocalDate.now() + today: LocalDate = LocalDate.now(), ): Boolean { return deleteColBackups(getBackups(File(colPath)), backupLimits, today) } @@ -430,17 +457,18 @@ open class BackupManager { private fun deleteColBackups( backups: Array, backupLimits: BackupLimits, - today: LocalDate + today: LocalDate, ): Boolean { - val unpackedBackups = backups.map { - // based on the format used, 0 is for "collection|backup" prefix and 1,2,3 are for - // year(4 digits), month(with 0 prefix, 1 is January) and day(with 0 prefix, starting from 1) - val nameSplits = it.nameWithoutExtension.split("-") - UnpackedBackup( - file = it, - date = LocalDate.of(nameSplits[1].toInt(), nameSplits[2].toInt(), nameSplits[3].toInt()) - ) - } + val unpackedBackups = + backups.map { + // based on the format used, 0 is for "collection|backup" prefix and 1,2,3 are for + // year(4 digits), month(with 0 prefix, 1 is January) and day(with 0 prefix, starting from 1) + val nameSplits = it.nameWithoutExtension.split("-") + UnpackedBackup( + file = it, + date = LocalDate.of(nameSplits[1].toInt(), nameSplits[2].toInt(), nameSplits[3].toInt()), + ) + } BackupFilter(today, backupLimits).getObsoleteBackups(unpackedBackups).forEach { backup -> if (!backup.file.delete()) { Timber.e("deleteColBackups() failed to delete %s", backup.file.absolutePath) @@ -458,7 +486,10 @@ open class BackupManager { * @return Whether all specified backups were successfully deleted. */ @Throws(IllegalArgumentException::class) - fun deleteBackups(collection: Collection, backupsToDelete: List): Boolean { + fun deleteBackups( + collection: Collection, + backupsToDelete: List, + ): Boolean { val allBackups = getBackups(File(collection.path)) val invalidBackupsToDelete = backupsToDelete.toSet() - allBackups.toSet() @@ -492,9 +523,10 @@ open class BackupManager { * in format such as "02 Nov 2022" instead of "11/2/22" or "2/11/22", which can be confusing. */ class LocalizedUnambiguousBackupTimeFormatter { - private val formatter = SimpleDateFormat( - DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM yyyy HH:mm") - ) + private val formatter = + SimpleDateFormat( + DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM yyyy HH:mm"), + ) fun getTimeOfBackupAsText(file: File): String { val backupDate = BackupManager.getBackupDate(file.name) ?: return file.name @@ -504,9 +536,10 @@ class LocalizedUnambiguousBackupTimeFormatter { private data class UnpackedBackup( val file: File, - val date: LocalDate + val date: LocalDate, ) : Comparable { override fun compareTo(other: UnpackedBackup): Int = date.compareTo(other.date) + private val epoch = LocalDate.ofEpochDay(0) fun day(): Long = ChronoUnit.DAYS.between(epoch, date) @@ -517,7 +550,9 @@ private data class UnpackedBackup( } enum class BackupStage { - Daily, Weekly, Monthly, + Daily, + Weekly, + Monthly, } // see https://github.com/ankitects/anki/blob/f3bb845961973bcfab34acfdc4d314294285ee74/rslib/src/collection/backup.rs#L186 @@ -547,18 +582,23 @@ private class BackupFilter(private val today: LocalDate, private var limits: Bac private fun isRecent(backup: UnpackedBackup): Boolean = backup.date == today - fun remaining(stage: BackupStage): Boolean = when (stage) { - BackupStage.Daily -> limits.daily > 0 - BackupStage.Weekly -> limits.weekly > 0 - BackupStage.Monthly -> limits.monthly > 0 - } - - fun markFreshOrObsolete(stage: BackupStage, backup: UnpackedBackup) { - val keep = when (stage) { - BackupStage.Daily -> backup.day() < lastKeptDay - BackupStage.Weekly -> backup.week() < lastKeptWeek - BackupStage.Monthly -> backup.month() < lastKeptMonth - } + fun remaining(stage: BackupStage): Boolean = + when (stage) { + BackupStage.Daily -> limits.daily > 0 + BackupStage.Weekly -> limits.weekly > 0 + BackupStage.Monthly -> limits.monthly > 0 + } + + fun markFreshOrObsolete( + stage: BackupStage, + backup: UnpackedBackup, + ) { + val keep = + when (stage) { + BackupStage.Daily -> backup.day() < lastKeptDay + BackupStage.Weekly -> backup.week() < lastKeptWeek + BackupStage.Monthly -> backup.month() < lastKeptMonth + } if (keep) { markFresh(stage, backup) } else { @@ -567,7 +607,10 @@ private class BackupFilter(private val today: LocalDate, private var limits: Bac } // Adjusts limits as per the stage of the kept backup, and last kept times. - fun markFresh(stage: BackupStage?, backup: UnpackedBackup) { + fun markFresh( + stage: BackupStage?, + backup: UnpackedBackup, + ) { lastKeptDay = backup.day() lastKeptWeek = backup.week() lastKeptMonth = backup.month() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index 65ef645a778f..12c22eb99010 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -1,3 +1,5 @@ + + /**************************************************************************************** * Copyright (c) 2010 Norbert Nagold * * Copyright (c) 2012 Kostas Spyropoulos * @@ -16,6 +18,8 @@ * this program. If not, see . * ****************************************************************************************/ +@file:Suppress("ktlint") + package com.ichi2.anki import android.content.* @@ -1613,6 +1617,7 @@ open class CardBrowser : updatePreviewMenuItem() invalidateOptionsMenu() // maybe the availability of undo changed } + private fun saveScrollingState(position: Int) { mOldCardId = viewModel.getCardIdAtPosition(position) mOldCardTopOffset = calculateTopOffset(position) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateBrowserAppearanceEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateBrowserAppearanceEditor.kt index 17706f1eae23..4f166c0cc1ac 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateBrowserAppearanceEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateBrowserAppearanceEditor.kt @@ -40,6 +40,7 @@ import timber.log.Timber class CardTemplateBrowserAppearanceEditor : AnkiActivity() { private lateinit var mQuestionEditText: EditText private lateinit var mAnswerEditText: EditText + override fun onCreate(savedInstanceState: Bundle?) { if (showedActivityFailedScreen(savedInstanceState)) { return @@ -162,10 +163,11 @@ class CardTemplateBrowserAppearanceEditor : AnkiActivity() { private fun saveAndExit() { Timber.i("Save and Exit") - val data = Intent().apply { - putExtra(INTENT_QUESTION_FORMAT, questionFormat) - putExtra(INTENT_ANSWER_FORMAT, answerFormat) - } + val data = + Intent().apply { + putExtra(INTENT_QUESTION_FORMAT, questionFormat) + putExtra(INTENT_ANSWER_FORMAT, answerFormat) + } setResult(RESULT_OK, data) finish() } @@ -215,14 +217,21 @@ class CardTemplateBrowserAppearanceEditor : AnkiActivity() { const val VALUE_USE_DEFAULT = "" @CheckResult - fun getIntentFromTemplate(context: Context, template: JSONObject): Intent { + fun getIntentFromTemplate( + context: Context, + template: JSONObject, + ): Intent { val browserQuestionTemplate = template.getString("bqfmt") val browserAnswerTemplate = template.getString("bafmt") return getIntent(context, browserQuestionTemplate, browserAnswerTemplate) } @CheckResult - fun getIntent(context: Context, questionFormat: String, answerFormat: String): Intent { + fun getIntent( + context: Context, + questionFormat: String, + answerFormat: String, + ): Intent { return Intent(context, CardTemplateBrowserAppearanceEditor::class.java).apply { putExtra(INTENT_QUESTION_FORMAT, questionFormat) putExtra(INTENT_ANSWER_FORMAT, answerFormat) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt index a31087c54424..ae7633abb5e6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt @@ -173,9 +173,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } mFieldNames = tempModel!!.notetype.fieldsNames // Set up the ViewPager with the sections adapter. - viewPager = findViewById(R.id.pager).apply { - adapter = TemplatePagerAdapter(this@CardTemplateEditor) - } + viewPager = + findViewById(R.id.pager).apply { + adapter = TemplatePagerAdapter(this@CardTemplateEditor) + } // Set activity title supportActionBar?.let { it.setTitle(R.string.title_activity_template_editor) @@ -196,13 +197,14 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { return tempModel != null && tempModel!!.notetype.toString() != oldModel.toString() } - private fun showDiscardChangesDialog() = DiscardChangesDialog.showDialog(this) { - Timber.i("TemplateEditor:: OK button pressed to confirm discard changes") - // Clear the edited model from any cache files, and clear it from this objects memory to discard changes - CardTemplateNotetype.clearTempModelFiles() - tempModel = null - finish() - } + private fun showDiscardChangesDialog() = + DiscardChangesDialog.showDialog(this) { + Timber.i("TemplateEditor:: OK button pressed to confirm discard changes") + // Clear the edited model from any cache files, and clear it from this objects memory to discard changes + CardTemplateNotetype.clearTempModelFiles() + tempModel = null + finish() + } /** When a deck is selected via Deck Override */ override fun onDeckSelected(deck: SelectableDeck?) { @@ -222,15 +224,16 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { return } - val message: String = if (deck == null) { - Timber.i("Removing default template from template '%s'", templateName) - template.put("did", JSONObject.NULL) - getString(R.string.model_manager_deck_override_removed_message, templateName) - } else { - Timber.i("Setting template '%s' to '%s'", templateName, deck.name) - template.put("did", deck.deckId) - getString(R.string.model_manager_deck_override_added_message, templateName, deck.name) - } + val message: String = + if (deck == null) { + Timber.i("Removing default template from template '%s'", templateName) + template.put("did", JSONObject.NULL) + getString(R.string.model_manager_deck_override_removed_message, templateName) + } else { + Timber.i("Setting template '%s' to '%s'", templateName, deck.name) + template.put("did", deck.deckId) + getString(R.string.model_manager_deck_override_added_message, templateName, deck.name) + } showSnackbar(message, Snackbar.LENGTH_SHORT) @@ -238,7 +241,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { invalidateOptionsMenu() } - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { if (keyCode == KeyEvent.KEYCODE_P) { if (event.isCtrlPressed) { val currentFragment = currentFragment @@ -250,21 +256,22 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { @get:VisibleForTesting val currentFragment: CardTemplateFragment? - get() = try { - supportFragmentManager.findFragmentByTag("f" + viewPager.currentItem) as CardTemplateFragment? - } catch (e: Exception) { - Timber.w("Failed to get current fragment") - null - } + get() = + try { + supportFragmentManager.findFragmentByTag("f" + viewPager.currentItem) as CardTemplateFragment? + } catch (e: Exception) { + Timber.w("Failed to get current fragment") + null + } // ---------------------------------------------------------------------------- // INNER CLASSES // ---------------------------------------------------------------------------- + /** * A [androidx.viewpager2.adapter.FragmentStateAdapter] that returns a fragment corresponding to * one of the tabs. */ inner class TemplatePagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { - private var mBaseId: Long = 0 override fun createFragment(position: Int): Fragment { @@ -300,19 +307,24 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { private lateinit var mTemplateEditor: CardTemplateEditor private var mTabLayoutMediator: TabLayoutMediator? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { // Storing a reference to the templateEditor allows us to use member variables mTemplateEditor = activity as CardTemplateEditor val mainView = inflater.inflate(R.layout.card_template_editor_item, container, false) val cardIndex = requireArguments().getInt(CARD_INDEX) val tempModel = mTemplateEditor.tempModel // Load template - val template: JSONObject = try { - tempModel!!.getTemplate(cardIndex) - } catch (e: JSONException) { - Timber.d(e, "Exception loading template in CardTemplateFragment. Probably stale fragment.") - return mainView - } + val template: JSONObject = + try { + tempModel!!.getTemplate(cardIndex) + } catch (e: JSONException) { + Timber.d(e, "Exception loading template in CardTemplateFragment. Probably stale fragment.") + return mainView + } mCurrentEditorTitle = mainView.findViewById(R.id.title_edit) mEditorEditText = mainView.findViewById(R.id.editor_editText) @@ -326,7 +338,12 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { mTemplateEditor.tabToViewId[cardIndex] = currentSelectedId when (currentSelectedId) { R.id.styling_edit -> setCurrentEditorView(currentSelectedId, tempModel.css, R.string.card_template_editor_styling) - R.id.back_edit -> setCurrentEditorView(currentSelectedId, template.getString("afmt"), R.string.card_template_editor_back) + R.id.back_edit -> + setCurrentEditorView( + currentSelectedId, + template.getString("afmt"), + R.string.card_template_editor_back, + ) else -> setCurrentEditorView(currentSelectedId, template.getString("qfmt"), R.string.card_template_editor_front) } // contents of menu have changed and menu should be redrawn @@ -337,25 +354,36 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { bottomNavigation.selectedItemId = requireArguments().getInt(EDITOR_VIEW_ID_KEY) // Set text change listeners - val templateEditorWatcher: TextWatcher = object : TextWatcher { - override fun afterTextChanged(arg0: Editable) { - mTemplateEditor.tabToCursorPosition[cardIndex] = mEditorEditText.selectionStart - when (currentEditorViewId) { - R.id.styling_edit -> tempModel.updateCss(mEditorEditText.text.toString()) - R.id.back_edit -> template.put("afmt", mEditorEditText.text) - else -> template.put("qfmt", mEditorEditText.text) + val templateEditorWatcher: TextWatcher = + object : TextWatcher { + override fun afterTextChanged(arg0: Editable) { + mTemplateEditor.tabToCursorPosition[cardIndex] = mEditorEditText.selectionStart + when (currentEditorViewId) { + R.id.styling_edit -> tempModel.updateCss(mEditorEditText.text.toString()) + R.id.back_edit -> template.put("afmt", mEditorEditText.text) + else -> template.put("qfmt", mEditorEditText.text) + } + mTemplateEditor.tempModel!!.updateTemplate(cardIndex, template) } - mTemplateEditor.tempModel!!.updateTemplate(cardIndex, template) - } - override fun beforeTextChanged(arg0: CharSequence, arg1: Int, arg2: Int, arg3: Int) { - /* do nothing */ - } + override fun beforeTextChanged( + arg0: CharSequence, + arg1: Int, + arg2: Int, + arg3: Int, + ) { + // do nothing + } - override fun onTextChanged(arg0: CharSequence, arg1: Int, arg2: Int, arg3: Int) { - /* do nothing */ + override fun onTextChanged( + arg0: CharSequence, + arg1: Int, + arg2: Int, + arg3: Int, + ) { + // do nothing + } } - } mEditorEditText.addTextChangedListener(templateEditorWatcher) return mainView @@ -368,11 +396,17 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { private inner class ActionModeCallback : ActionMode.Callback { private val mInsertFieldId = 1 - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + override fun onCreateActionMode( + mode: ActionMode, + menu: Menu, + ): Boolean { return true } - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + override fun onPrepareActionMode( + mode: ActionMode, + menu: Menu, + ): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && menu.findItem(mInsertFieldId) != null) { return false } @@ -386,7 +420,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { return initialSize != menu.size() } - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + override fun onActionItemClicked( + mode: ActionMode, + item: MenuItem, + ): Boolean { val itemId = item.itemId return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && itemId == mInsertFieldId) { showInsertFieldDialog() @@ -403,7 +440,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } @NeedsTest( - "the kotlin migration made this method crash due to a recursive call when the dialog would return its data" + "the kotlin migration made this method crash due to a recursive call when the dialog would return its data", ) private fun showInsertFieldDialog() { mTemplateEditor.mFieldNames?.let { fieldNames -> @@ -420,7 +457,11 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { mEditorEditText.text!!.replace(min(start, end), max(start, end), updatedString, 0, updatedString.length) } - fun setCurrentEditorView(id: Int, editorContent: String, editorTitleId: Int) { + fun setCurrentEditorView( + id: Int, + editorContent: String, + editorTitleId: Int, + ) { currentEditorViewId = id mEditorEditText.setText(editorContent) mCurrentEditorTitle!!.text = resources.getString(editorTitleId) @@ -428,7 +469,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { mEditorEditText.requestFocus() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { initTabLayoutMediator() parentFragmentManager.setFragmentResultListener(REQUEST_FIELD_INSERT, viewLifecycleOwner) { key, bundle -> if (key == REQUEST_FIELD_INSERT) { @@ -443,9 +487,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { if (mTabLayoutMediator != null) { mTabLayoutMediator!!.detach() } - mTabLayoutMediator = TabLayoutMediator(mTemplateEditor.mSlidingTabLayout!!, mTemplateEditor.viewPager) { tab: TabLayout.Tab, position: Int -> - tab.text = mTemplateEditor.tempModel!!.getTemplate(position).getString("name") - } + mTabLayoutMediator = + TabLayoutMediator(mTemplateEditor.mSlidingTabLayout!!, mTemplateEditor.viewPager) { tab: TabLayout.Tab, position: Int -> + tab.text = mTemplateEditor.tempModel!!.getTemplate(position).getString("name") + } mTabLayoutMediator!!.attach() } @@ -453,7 +498,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { // Enable menu (requireActivity() as MenuHost).addMenuProvider( object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + override fun onCreateMenu( + menu: Menu, + menuInflater: MenuInflater, + ) { menu.clear() menuInflater.inflate(R.menu.card_template_editor, menu) @@ -464,11 +512,12 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } else { val template = getCurrentTemplate() - @StringRes val overrideStringRes = if (template != null && template.has("did") && !template.isNull("did")) { - R.string.card_template_editor_deck_override_on - } else { - R.string.card_template_editor_deck_override_off - } + @StringRes val overrideStringRes = + if (template != null && template.has("did") && !template.isNull("did")) { + R.string.card_template_editor_deck_override_on + } else { + R.string.card_template_editor_deck_override_off + } menu.findItem(R.id.action_add_deck_override).setTitle(overrideStringRes) } @@ -492,11 +541,12 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { val ordinal = mTemplateEditor.viewPager.currentItem // isOrdinalPendingAdd method will check if there are any new card types added or not, // if TempModel has new card type then numAffectedCards will be 0 by default. - val numAffectedCards = if (!CardTemplateNotetype.isOrdinalPendingAdd(tempModel!!, ordinal)) { - col.notetypes.tmplUseCount(tempModel.notetype, ordinal) - } else { - 0 - } + val numAffectedCards = + if (!CardTemplateNotetype.isOrdinalPendingAdd(tempModel!!, ordinal)) { + col.notetypes.tmplUseCount(tempModel.notetype, ordinal) + } else { + 0 + } confirmAddCards(tempModel.notetype, numAffectedCards) return true } @@ -517,12 +567,13 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } // Show confirmation dialog - val numAffectedCards = if (!CardTemplateNotetype.isOrdinalPendingAdd(tempModel, ordinal)) { - Timber.d("Ordinal is not a pending add, so we'll get the current card count for confirmation") - col.notetypes.tmplUseCount(tempModel.notetype, ordinal) - } else { - 0 - } + val numAffectedCards = + if (!CardTemplateNotetype.isOrdinalPendingAdd(tempModel, ordinal)) { + Timber.d("Ordinal is not a pending add, so we'll get the current card count for confirmation") + col.notetypes.tmplUseCount(tempModel.notetype, ordinal) + } else { + 0 + } confirmDeleteCards(template, tempModel.notetype, numAffectedCards) return true } @@ -571,7 +622,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } }, viewLifecycleOwner, - Lifecycle.State.RESUMED + Lifecycle.State.RESUMED, ) } @@ -610,7 +661,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { onRequestPreviewResult.launch(i) } - private fun displayDeckOverrideDialog(col: Collection, tempModel: CardTemplateNotetype) { + private fun displayDeckOverrideDialog( + col: Collection, + tempModel: CardTemplateNotetype, + ) { val activity = requireActivity() as AnkiActivity if (tempModel.notetype.isCloze) { showSnackbar(getString(R.string.multimedia_editor_something_wrong), Snackbar.LENGTH_SHORT) @@ -663,7 +717,11 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { return requireArguments().getInt(CARD_INDEX) } - private fun deletionWouldOrphanNote(col: Collection, tempModel: CardTemplateNotetype?, position: Int): Boolean { + private fun deletionWouldOrphanNote( + col: Collection, + tempModel: CardTemplateNotetype?, + position: Int, + ): Boolean { // For existing templates, make sure we won't leave orphaned notes if we delete the template // // Note: we are in-memory, so the database is unaware of previous but unsaved deletes. @@ -726,16 +784,21 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { * @param notetype model to remove template from, modified in place by reference * @param numAffectedCards number of cards which will be affected */ - private fun confirmDeleteCards(tmpl: JSONObject, notetype: NotetypeJson, numAffectedCards: Int) { + private fun confirmDeleteCards( + tmpl: JSONObject, + notetype: NotetypeJson, + numAffectedCards: Int, + ) { val d = ConfirmationDialog() - val msg = String.format( - resources.getQuantityString( - R.plurals.card_template_editor_confirm_delete, - numAffectedCards - ), - numAffectedCards, - tmpl.optString("name") - ) + val msg = + String.format( + resources.getQuantityString( + R.plurals.card_template_editor_confirm_delete, + numAffectedCards, + ), + numAffectedCards, + tmpl.optString("name"), + ) d.setArgs(msg) val deleteCard = Runnable { deleteTemplate(tmpl, notetype) } @@ -749,15 +812,19 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { * @param notetype model to add new template and modified in place by reference * @param numAffectedCards number of cards which will be affected */ - private fun confirmAddCards(notetype: NotetypeJson, numAffectedCards: Int) { + private fun confirmAddCards( + notetype: NotetypeJson, + numAffectedCards: Int, + ) { val d = ConfirmationDialog() - val msg = String.format( - resources.getQuantityString( - R.plurals.card_template_editor_confirm_add, - numAffectedCards - ), - numAffectedCards - ) + val msg = + String.format( + resources.getQuantityString( + R.plurals.card_template_editor_confirm_add, + numAffectedCards, + ), + numAffectedCards, + ) d.setArgs(msg) val addCard = Runnable { addNewTemplate(notetype) } @@ -778,11 +845,12 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { e.log() val d = ConfirmationDialog() d.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = Runnable { - mTemplateEditor.getColUnsafe.modSchemaNoCheck() - schemaChangingAction.run() - mTemplateEditor.dismissAllDialogFragments() - } + val confirm = + Runnable { + mTemplateEditor.getColUnsafe.modSchemaNoCheck() + schemaChangingAction.run() + mTemplateEditor.dismissAllDialogFragments() + } val cancel = Runnable { mTemplateEditor.dismissAllDialogFragments() } d.setConfirm(confirm) d.setCancel(cancel) @@ -794,7 +862,10 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { * @param tmpl template to remove * @param notetype model to remove from, updated in place by reference */ - private fun deleteTemplate(tmpl: JSONObject, notetype: NotetypeJson) { + private fun deleteTemplate( + tmpl: JSONObject, + notetype: NotetypeJson, + ) { val oldTemplates = notetype.getJSONArray("tmpls") val newTemplates = JSONArray() for (possibleMatch in oldTemplates.jsonObjectIterable()) { @@ -881,7 +952,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { cardIndex: Int, noteId: NoteId, cursorPosition: Int, - viewId: Int + viewId: Int, ): CardTemplateFragment { val f = CardTemplateFragment() val args = Bundle() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt index 1387ca033084..44f294c753f0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt @@ -34,16 +34,18 @@ import java.io.IOException /** A wrapper for a notetype in JSON format with helpers for editing the notetype. */ class CardTemplateNotetype(val notetype: NotetypeJson) { enum class ChangeType { - ADD, DELETE + ADD, + DELETE, } private var mTemplateChanges = ArrayList>() var editedModelFileName: String? = null - fun toBundle(): Bundle = bundleOf( - INTENT_MODEL_FILENAME to saveTempModel(AnkiDroidApp.instance.applicationContext, notetype), - "mTemplateChanges" to mTemplateChanges - ) + fun toBundle(): Bundle = + bundleOf( + INTENT_MODEL_FILENAME to saveTempModel(AnkiDroidApp.instance.applicationContext, notetype), + "mTemplateChanges" to mTemplateChanges, + ) private fun loadTemplateChanges(bundle: Bundle) { try { @@ -71,7 +73,10 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { val css: String get() = notetype.getString("css") - fun updateTemplate(ordinal: Int, template: JSONObject) { + fun updateTemplate( + ordinal: Int, + template: JSONObject, + ) { notetype.getJSONArray("tmpls").put(ordinal, template) } @@ -96,7 +101,10 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { * Template deletes shift card ordinals in the database. To operate without saving, we must keep track to apply in order. * In addition, we don't want to persist a template add just to delete it later, so we combine those if they happen */ - fun addTemplateChange(type: ChangeType, ordinal: Int) { + fun addTemplateChange( + type: ChangeType, + ordinal: Int, + ) { Timber.d("addTemplateChange() type %s for ordinal %s", type, ordinal) val templateChanges = templateChanges val change = arrayOf(ordinal, type) @@ -107,16 +115,18 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { for (i in templateChanges.indices.reversed()) { val oldChange = templateChanges[i] when (oldChange[1] as ChangeType) { - ChangeType.DELETE -> if (oldChange[0] as Int - ordinalAdjustment <= ordinal) { - // Deleting an ordinal at or below us? Adjust our comparison basis... - ordinalAdjustment++ - continue - } - ChangeType.ADD -> if (ordinal == oldChange[0] as Int - ordinalAdjustment) { - // Deleting something we added this session? Edit it out via compaction - compactTemplateChanges(oldChange[0] as Int) - return - } + ChangeType.DELETE -> + if (oldChange[0] as Int - ordinalAdjustment <= ordinal) { + // Deleting an ordinal at or below us? Adjust our comparison basis... + ordinalAdjustment++ + continue + } + ChangeType.ADD -> + if (ordinal == oldChange[0] as Int - ordinalAdjustment) { + // Deleting something we added this session? Edit it out via compaction + compactTemplateChanges(oldChange[0] as Int) + return + } } } } @@ -190,7 +200,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { "dumpChanges() During save change %s will be ord/type %s/%s", i, adjustedChange[0], - adjustedChange[1] + adjustedChange[1], ) } } @@ -225,7 +235,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { "getAdjustedTemplateChanges() change %s ordinal adjusted from %s to %s", i, change[0], - adjustedChange[0] + adjustedChange[0], ) } ChangeType.DELETE -> {} @@ -242,7 +252,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { private fun compactTemplateChanges(addedOrdinalToDelete: Int) { Timber.d( "compactTemplateChanges() merge/purge add/delete ordinal added as %s", - addedOrdinalToDelete + addedOrdinalToDelete, ) var postChange = false var ordinalAdjustment = 0 @@ -271,7 +281,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { ordinalAdjustment++ Timber.d( "compactTemplateChanges() delete affecting purged template, shifting basis, adj: %s", - ordinalAdjustment + ordinalAdjustment, ) } @@ -301,12 +311,13 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { return null } Timber.d("onCreate() loading saved model file %s", editedModelFileName) - val tempNotetypeJSON: NotetypeJson = try { - getTempModel(editedModelFileName) - } catch (e: IOException) { - Timber.w(e, "Unable to load saved model file") - return null - } + val tempNotetypeJSON: NotetypeJson = + try { + getTempModel(editedModelFileName) + } catch (e: IOException) { + Timber.w(e, "Unable to load saved model file") + return null + } val model = CardTemplateNotetype(tempNotetypeJSON) model.loadTemplateChanges(bundle) return model @@ -316,7 +327,10 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { * Save the current model to a temp file in the application internal cache directory * @return String representing the absolute path of the saved file, or null if there was a problem */ - fun saveTempModel(context: Context, tempModel: JSONObject): String? { + fun saveTempModel( + context: Context, + tempModel: JSONObject, + ): String? { Timber.d("saveTempModel() saving tempModel") var tempModelFile: File try { @@ -372,7 +386,10 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { * @param ord int representing an ordinal in the model, that might be an unsaved addition * @return boolean true if it is a pending addition from this editing session */ - fun isOrdinalPendingAdd(model: CardTemplateNotetype, ord: Int): Boolean { + fun isOrdinalPendingAdd( + model: CardTemplateNotetype, + ord: Int, + ): Boolean { for (i in model.templateChanges.indices) { // commented out to make the code compile, why is this unused? // val change = model.templateChanges[i] @@ -381,7 +398,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { Timber.d( "isOrdinalPendingAdd() found ord %s was pending add (would adjust to %s)", ord, - adjustedOrdinal + adjustedOrdinal, ) return true } @@ -396,7 +413,10 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { * @param changesIndex the index of the template in the changes array * @return either ordinal adjusted by any pending deletes if it is a pending add, or -1 if the ordinal is not an add */ - fun getAdjustedAddOrdinalAtChangeIndex(model: CardTemplateNotetype, changesIndex: Int): Int { + fun getAdjustedAddOrdinalAtChangeIndex( + model: CardTemplateNotetype, + changesIndex: Int, + ): Int { if (changesIndex >= model.templateChanges.size) { return -1 } @@ -416,24 +436,25 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { Timber.d( "getAdjustedAddOrdinalAtChangeIndex() contemplating delete at index %s, current ord adj %s", i, - ordinalAdjustment - ) - } - ChangeType.ADD -> if (changesIndex == i) { - // something we added this session - Timber.d( - "getAdjustedAddOrdinalAtChangeIndex() pending add found at at index %s, old ord/adjusted ord %s/%s", - i, - currentOrdinal, - currentOrdinal - ordinalAdjustment + ordinalAdjustment, ) - return currentOrdinal - ordinalAdjustment } + ChangeType.ADD -> + if (changesIndex == i) { + // something we added this session + Timber.d( + "getAdjustedAddOrdinalAtChangeIndex() pending add found at at index %s, old ord/adjusted ord %s/%s", + i, + currentOrdinal, + currentOrdinal - ordinalAdjustment, + ) + return currentOrdinal - ordinalAdjustment + } } } Timber.d( "getAdjustedAddOrdinalAtChangeIndex() determined changesIndex %s was not a pending add", - changesIndex + changesIndex, ) return -1 } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt index d8d98463f510..51a635c90c5d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplatePreviewer.kt @@ -71,6 +71,7 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { private var mAllFieldsNull = true private var mCardType: String? = null protected var previewLayout: PreviewLayout? = null + override fun onCreate(savedInstanceState: Bundle?) { if (showedActivityFailedScreen(savedInstanceState)) { return @@ -165,20 +166,21 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { } override fun hideEaseButtons() { - /* do nothing */ + // do nothing } override fun displayAnswerBottomBar() { - /* do nothing */ + // do nothing } - private val mToggleAnswerHandler = View.OnClickListener { - if (mShowingAnswer) { - displayCardQuestion() - } else { - displayCardAnswer() + private val mToggleAnswerHandler = + View.OnClickListener { + if (mShowingAnswer) { + displayCardQuestion() + } else { + displayCardAnswer() + } } - } /** When the next template is requested */ fun onNextCard() { @@ -251,7 +253,9 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { if (mCardList != null && mCardListIndex >= 0 && mCardListIndex < mCardList!!.size) { currentCard = PreviewerCard(col, mCardList!![mCardListIndex]) } else if (mEditedNotetype != null) { // bare note type (not coming from note editor), or new card template - Timber.d("onCreate() CardTemplatePreviewer started with edited model and template index, displaying blank to preview formatting") + Timber.d( + "onCreate() CardTemplatePreviewer started with edited model and template index, displaying blank to preview formatting", + ) currentCard = getDummyCard(mEditedNotetype!!, mOrdinal) if (currentCard == null) { showThemedToast(applicationContext, getString(R.string.invalid_template), false) @@ -271,7 +275,10 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { showBackIcon() } - protected fun getCard(col: Collection, cardListIndex: Long): Card { + protected fun getCard( + col: Collection, + cardListIndex: Long, + ): Card { return PreviewerCard(col, cardListIndex) } @@ -298,7 +305,10 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { * assumption on the content of tagsList, except that its strings * are valid tags (i.e. no spaces in it). */ - private fun setTags(currentNote: Note, tagsList: List?) { + private fun setTags( + currentNote: Note, + tagsList: List?, + ) { val currentTags = currentNote.tags.toTypedArray() for (tag in currentTags) { currentNote.delTag(tag) @@ -319,11 +329,16 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { } private fun getBundleEditFields(noteEditorBundle: Bundle?): MutableList { - val noteFields = noteEditorBundle!!.getBundle("editFields") - ?: return mutableListOf() + val noteFields = + noteEditorBundle!!.getBundle("editFields") + ?: return mutableListOf() // we map from "int" -> field, but the order isn't guaranteed, and there may be skips. // so convert this to a list of strings, with null in place of the invalid fields - val elementCount = noteFields.keySet().stream().map { s: String -> s.toInt() }.max { obj: Int, anotherInteger: Int? -> obj.compareTo(anotherInteger!!) }.orElse(-1) + 1 + val elementCount = + noteFields.keySet().stream().map { + s: String -> + s.toInt() + }.max { obj: Int, anotherInteger: Int? -> obj.compareTo(anotherInteger!!) }.orElse(-1) + 1 val ret = Array(elementCount) { "" } // init array, nulls cause a crash for (fieldOrd in noteFields.keySet()) { ret[fieldOrd.toInt()] = noteFields.getString(fieldOrd)!! @@ -331,14 +346,19 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { return mutableListOf(*ret) } - private fun indexFromOrdinal(col: Collection, fieldsBundle: Bundle, ordinal: Int): Int { + private fun indexFromOrdinal( + col: Collection, + fieldsBundle: Bundle, + ordinal: Int, + ): Int { return when (mEditedNotetype?.isCloze) { true -> { - val note = col.newNote(mEditedNotetype!!).apply { - for ((index, field) in getBundleEditFields(fieldsBundle).withIndex()) { - this.setField(index, field) + val note = + col.newNote(mEditedNotetype!!).apply { + for ((index, field) in getBundleEditFields(fieldsBundle).withIndex()) { + this.setField(index, field) + } } - } val clozeNumber = mOrdinal + 1 col.clozeNumbersInNote(note).indexOf(clozeNumber) } @@ -350,7 +370,10 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { * This method generates a note from a sample model, or fails if invalid * @param index The index in the templates for the model. NOT `ord` */ - fun getDummyCard(notetype: NotetypeJson, index: Int): Card? { + fun getDummyCard( + notetype: NotetypeJson, + index: Int, + ): Card? { return getDummyCard(notetype, index, notetype.fieldsNames.toMutableList()) } @@ -358,7 +381,11 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { * This method generates a note from a sample model, or fails if invalid * @param cardIndex The index in the templates for the model. NOT `ord` */ - private fun getDummyCard(notetype: NotetypeJson?, cardIndex: Int, fieldValues: MutableList): Card? { + private fun getDummyCard( + notetype: NotetypeJson?, + cardIndex: Int, + fieldValues: MutableList, + ): Card? { Timber.d("getDummyCard() Creating dummy note for index %s", cardIndex) if (notetype == null) { return null @@ -395,10 +422,8 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { inner class PreviewerCard(col: Collection, id: Long) : Card(col, id) { private val mNote: Note? = null - /* if we have an unsaved note saved, use it instead of a collection lookup */ - override fun note( - reload: Boolean - ): Note { + // if we have an unsaved note saved, use it instead of a collection lookup + override fun note(reload: Boolean): Note { return mNote ?: super.note(reload) } @@ -416,20 +441,25 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { return mEditedNotetype ?: super.model() } - override fun renderOutput(reload: Boolean, browser: Boolean): TemplateRenderOutput { + override fun renderOutput( + reload: Boolean, + browser: Boolean, + ): TemplateRenderOutput { if (renderOutput == null || reload) { - val index = if (model().isCloze) { - 0 - } else { - ord - } - val context = TemplateManager.TemplateRenderContext.fromCardLayout( - note(), - this, - model(), - model().getJSONArray("tmpls")[index] as JSONObject, - fillEmpty = false - ) + val index = + if (model().isCloze) { + 0 + } else { + ord + } + val context = + TemplateManager.TemplateRenderContext.fromCardLayout( + note(), + this, + model(), + model().getJSONArray("tmpls")[index] as JSONObject, + fillEmpty = false, + ) renderOutput = context.render() } @@ -439,32 +469,42 @@ open class CardTemplatePreviewer : AbstractFlashcardViewer() { } private class EphemeralCard(col: Collection, id: Long?) : Card(col, id) { - override fun renderOutput(reload: Boolean, browser: Boolean): TemplateRenderOutput { + override fun renderOutput( + reload: Boolean, + browser: Boolean, + ): TemplateRenderOutput { return this.renderOutput!! } + companion object { - fun fromNote(n: Note, col: Collection, cardIndex: Int = 0): EphemeralCard { + fun fromNote( + n: Note, + col: Collection, + cardIndex: Int = 0, + ): EphemeralCard { val card = EphemeralCard(col, null) card.did = 1 card.ord = n.cardIndexToOrd(cardIndex) Timber.v("Generating ephemeral note, idx %d ord %d", cardIndex, card.ord) val nt = n.notetype - val templateIdx = if (nt.type == Consts.MODEL_CLOZE) { - 0 - } else { - cardIndex - } + val templateIdx = + if (nt.type == Consts.MODEL_CLOZE) { + 0 + } else { + cardIndex + } val template = nt.tmpls[templateIdx] as JSONObject template.put("ord", card.ord) - val output = TemplateManager.TemplateRenderContext.fromCardLayout( - n, - card, - notetype = nt, - template = template, - fillEmpty = false - ).render() + val output = + TemplateManager.TemplateRenderContext.fromCardLayout( + n, + card, + notetype = nt, + template = template, + fillEmpty = false, + ).render() card.renderOutput = output card.setNote(n) return card diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt index 1e1228c23f3f..25b500dbc4ec 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt @@ -67,7 +67,10 @@ open class CollectionHelper { * @return the [Collection] if it could be obtained, `null` otherwise. */ @Synchronized - fun tryGetColUnsafe(context: Context?, reportException: Boolean = true): Collection? { + fun tryGetColUnsafe( + context: Context?, + reportException: Boolean = true, + ): Collection? { lastOpenFailure = null return try { getColUnsafe(context) @@ -159,26 +162,27 @@ open class CollectionHelper { val defaultRequiredFreeSpace = defaultRequiredFreeSpace(context) return context.resources.getString( R.string.integrity_check_insufficient_space, - defaultRequiredFreeSpace + defaultRequiredFreeSpace, ) } val required = Formatter.formatShortFileSize(context, mRequiredSpace) - val insufficientSpace = context.resources.getString( - R.string.integrity_check_insufficient_space, - required - ) + val insufficientSpace = + context.resources.getString( + R.string.integrity_check_insufficient_space, + required, + ) // Also concat in the extra content showing the current free space. val currentFree = Formatter.formatShortFileSize(context, mFreeSpace) - val insufficientSpaceCurrentFree = context.resources.getString( - R.string.integrity_check_insufficient_space_extra_content, - currentFree - ) + val insufficientSpaceCurrentFree = + context.resources.getString( + R.string.integrity_check_insufficient_space_extra_content, + currentFree, + ) return insufficientSpace + insufficientSpaceCurrentFree } companion object { - private fun fromError(errorMessage: String): CollectionIntegrityStorageCheck { return CollectionIntegrityStorageCheck(errorMessage) } @@ -197,8 +201,8 @@ open class CollectionHelper { return fromError( context.resources.getString( R.string.integrity_check_insufficient_space, - requiredFreeSpace - ) + requiredFreeSpace, + ), ) } @@ -215,8 +219,8 @@ open class CollectionHelper { return fromError( context.resources.getString( R.string.integrity_check_insufficient_space, - readableFileSize - ) + readableFileSize, + ), ) } return CollectionIntegrityStorageCheck(requiredSpaceInBytes, freeSpace) @@ -225,7 +229,10 @@ open class CollectionHelper { } enum class CollectionOpenFailure { - FILE_TOO_NEW, CORRUPT, LOCKED, DISK_FULL + FILE_TOO_NEW, + CORRUPT, + LOCKED, + DISK_FULL, } @VisibleForTesting(otherwise = VisibleForTesting.NONE) @@ -489,23 +496,27 @@ open class CollectionHelper { // This uses a lambda as we typically depends on the `lateinit` AnkiDroidApp.instance // If we remove all Android references, we get a significant unit test speedup @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal fun getCurrentAnkiDroidDirectoryOptionalContext(preferences: SharedPreferences, context: () -> Context): String { + internal fun getCurrentAnkiDroidDirectoryOptionalContext( + preferences: SharedPreferences, + context: () -> Context, + ): String { return if (AnkiDroidApp.INSTRUMENTATION_TESTING) { // create an "androidTest" directory inside the current collection directory which contains the test data // "/AnkiDroid/androidTest" would be a new collection path - val currentCollectionDirectory = preferences.getOrSetString(PREF_COLLECTION_PATH) { - getDefaultAnkiDroidDirectory(context()) - } + val currentCollectionDirectory = + preferences.getOrSetString(PREF_COLLECTION_PATH) { + getDefaultAnkiDroidDirectory(context()) + } File( currentCollectionDirectory, - "androidTest" + "androidTest", ).absolutePath } else if (ankiDroidDirectoryOverride != null) { ankiDroidDirectoryOverride!! } else { preferences.getOrSetString(PREF_COLLECTION_PATH) { getDefaultAnkiDroidDirectory( - context() + context(), ) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt index db407a49bd20..e69f98510c04 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt @@ -85,7 +85,9 @@ object CollectionManager { * * context(Queue) suspend fun canOnlyBeRunInWithQueue() */ - private suspend fun withQueue(@WorkerThread block: CollectionManager.() -> T): T { + private suspend fun withQueue( + @WorkerThread block: CollectionManager.() -> T, + ): T { return withContext(queue) { this@CollectionManager.block() } @@ -100,7 +102,9 @@ object CollectionManager { * sure the collection won't be closed or modified by another thread. This guarantee * does not hold if legacy code calls [getColUnsafe]. */ - suspend fun withCol(@WorkerThread block: Collection.() -> T): T { + suspend fun withCol( + @WorkerThread block: Collection.() -> T, + ): T { return withQueue { ensureOpenInner() block(collection!!) @@ -114,7 +118,9 @@ object CollectionManager { * these two cases, it should wrap the return value of the block in a class (eg Optional), * instead of returning a nullable object. */ - suspend fun withOpenColOrNull(@WorkerThread block: Collection.() -> T): T? { + suspend fun withOpenColOrNull( + @WorkerThread block: Collection.() -> T, + ): T? { return withQueue { if (collection != null && !collection!!.dbClosed) { block(collection!!) @@ -148,7 +154,10 @@ object CollectionManager { return getBackend().tr } - fun compareAnswer(expected: String, given: String): String { + fun compareAnswer( + expected: String, + given: String, + ): String { // bypass the lock, as the type answer code is heavily nested in non-suspend functions return getBackend().compareAnswer(expected, given) } @@ -305,22 +314,24 @@ object CollectionManager { val stackTraceElements = Thread.currentThread().stackTrace // locate the probable calling file/line in the stack trace, by filtering // out our own code, and standard dalvik/java.lang stack frames - val caller = stackTraceElements.filter { - val klass = it.className - val toCheck = listOf( - "CollectionManager", - "dalvik", - "java.lang", - "CollectionHelper", - "AnkiActivity" - ) - for (text in toCheck) { - if (text in klass) { - return@filter false + val caller = + stackTraceElements.filter { + val klass = it.className + val toCheck = + listOf( + "CollectionManager", + "dalvik", + "java.lang", + "CollectionHelper", + "AnkiActivity", + ) + for (text in toCheck) { + if (text in klass) { + return@filter false + } } - } - true - }.first() + true + }.first() Timber.w("blocked main thread for %dms:\n%s", elapsed, caller) } } @@ -370,7 +381,10 @@ object CollectionManager { * other code can open the collection while the operation runs. Reopens * at the end, and rolls back the path change if reopening fails. */ - suspend fun migrateEssentialFiles(context: Context, folders: ValidatedMigrationSourceAndDestination) { + suspend fun migrateEssentialFiles( + context: Context, + folders: ValidatedMigrationSourceAndDestination, + ) { withQueue { ensureClosedInner() val migrator = MigrateEssentialFiles(context, folders) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt index aa1e0b3fad8e..4212708ab809 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt @@ -51,7 +51,7 @@ import kotlin.coroutines.suspendCoroutine fun CoroutineScope.launchCatching( block: suspend () -> Unit, dispatcher: CoroutineDispatcher = Dispatchers.Default, - errorMessageHandler: suspend (String) -> Unit + errorMessageHandler: suspend (String) -> Unit, ): Job { return launch(dispatcher) { try { @@ -74,12 +74,12 @@ fun CoroutineScope.launchCatching( fun ViewModel.launchCatching( block: suspend T.() -> Unit, dispatcher: CoroutineDispatcher = Dispatchers.Default, - errorMessageHandler: suspend (String) -> Unit + errorMessageHandler: suspend (String) -> Unit, ): Job { return viewModelScope.launchCatching( { block.invoke(this as T) }, dispatcher, - errorMessageHandler + errorMessageHandler, ) } @@ -95,7 +95,7 @@ fun ViewModel.launchCatching( */ suspend fun FragmentActivity.runCatchingTask( errorMessage: String? = null, - block: suspend () -> T? + block: suspend () -> T?, ): T? { try { return block() @@ -136,24 +136,26 @@ suspend fun FragmentActivity.runCatchingTask( * @return [CoroutineExceptionHandler] * @see [FragmentActivity.launchCatchingTask] */ -fun getCoroutineExceptionHandler(activity: Activity, errorMessage: String? = null) = - CoroutineExceptionHandler { _, throwable -> - // No need to check for cancellation-exception, it does not gets caught by CoroutineExceptionHandler - when (throwable) { - is BackendInterruptedException -> { - Timber.e(throwable, errorMessage) - throwable.localizedMessage?.let { activity.showSnackbar(it) } - } - is BackendException -> { - Timber.e(throwable, errorMessage) - showError(activity, throwable.localizedMessage!!, throwable) - } - else -> { - Timber.e(throwable, errorMessage) - showError(activity, throwable.toString(), throwable) - } +fun getCoroutineExceptionHandler( + activity: Activity, + errorMessage: String? = null, +) = CoroutineExceptionHandler { _, throwable -> + // No need to check for cancellation-exception, it does not gets caught by CoroutineExceptionHandler + when (throwable) { + is BackendInterruptedException -> { + Timber.e(throwable, errorMessage) + throwable.localizedMessage?.let { activity.showSnackbar(it) } + } + is BackendException -> { + Timber.e(throwable, errorMessage) + showError(activity, throwable.localizedMessage!!, throwable) + } + else -> { + Timber.e(throwable, errorMessage) + showError(activity, throwable.toString(), throwable) } } +} /** * Launch a job that catches any uncaught errors and reports them to the user. @@ -162,7 +164,7 @@ fun getCoroutineExceptionHandler(activity: Activity, errorMessage: String? = nul */ fun FragmentActivity.launchCatchingTask( errorMessage: String? = null, - block: suspend CoroutineScope.() -> Unit + block: suspend CoroutineScope.() -> Unit, ): Job { return lifecycle.coroutineScope.launch { runCatchingTask(errorMessage) { block() } @@ -172,14 +174,19 @@ fun FragmentActivity.launchCatchingTask( /** See [FragmentActivity.launchCatchingTask] */ fun Fragment.launchCatchingTask( errorMessage: String? = null, - block: suspend CoroutineScope.() -> Unit + block: suspend CoroutineScope.() -> Unit, ): Job { return lifecycle.coroutineScope.launch { requireActivity().runCatchingTask(errorMessage) { block() } } } -private fun showError(context: Context, msg: String, exception: Throwable, crashReport: Boolean = true) { +private fun showError( + context: Context, + msg: String, + exception: Throwable, + crashReport: Boolean = true, +) { try { AlertDialog.Builder(context).show { title(R.string.vague_error) @@ -189,7 +196,7 @@ private fun showError(context: Context, msg: String, exception: Throwable, crash setOnDismissListener { CrashReportService.sendExceptionReport( exception, - origin = context::class.java.simpleName + origin = context::class.java.simpleName, ) } } @@ -207,12 +214,13 @@ private fun showError(context: Context, msg: String, exception: Throwable, crash suspend fun Backend.withProgress( extractProgress: ProgressContext.() -> Unit, updateUi: ProgressContext.() -> Unit, - block: suspend CoroutineScope.() -> T + block: suspend CoroutineScope.() -> T, ): T { return coroutineScope { - val monitor = launch { - monitorProgress(this@withProgress, extractProgress, updateUi) - } + val monitor = + launch { + monitorProgress(this@withProgress, extractProgress, updateUi) + } try { block() } finally { @@ -231,20 +239,23 @@ suspend fun Backend.withProgress( suspend fun FragmentActivity.withProgress( extractProgress: ProgressContext.() -> Unit, onCancel: ((Backend) -> Unit)? = { it.setWantsAbort() }, - op: suspend () -> T + op: suspend () -> T, ): T { val backend = CollectionManager.getBackend() return withProgressDialog( context = this@withProgress, - onCancel = if (onCancel != null) { - fun() { onCancel(backend) } - } else { - null - } + onCancel = + if (onCancel != null) { + fun() { + onCancel(backend) + } + } else { + null + }, ) { dialog -> backend.withProgress( extractProgress = extractProgress, - updateUi = { updateDialog(dialog) } + updateUi = { updateDialog(dialog) }, ) { op() } @@ -260,64 +271,74 @@ suspend fun FragmentActivity.withProgress( */ suspend fun Activity.withProgress( message: String = resources.getString(R.string.dialog_processing), - op: suspend () -> T -): T = withProgressDialog( - context = this@withProgress, - onCancel = null -) { dialog -> - @Suppress("Deprecation") // ProgressDialog deprecation - dialog.setMessage(message) - op() -} + op: suspend () -> T, +): T = + withProgressDialog( + context = this@withProgress, + onCancel = null, + ) { dialog -> + @Suppress("Deprecation") // ProgressDialog deprecation + dialog.setMessage(message) + op() + } /** @see withProgress(String, ...) */ -suspend fun Fragment.withProgress(message: String, block: suspend () -> T): T = - requireActivity().withProgress(message, block) +suspend fun Fragment.withProgress( + message: String, + block: suspend () -> T, +): T = requireActivity().withProgress(message, block) /** @see withProgress(String, ...) */ -suspend fun Activity.withProgress(@StringRes messageId: Int, block: suspend () -> T): T = - withProgress(resources.getString(messageId), block) +suspend fun Activity.withProgress( + @StringRes messageId: Int, + block: suspend () -> T, +): T = withProgress(resources.getString(messageId), block) /** @see withProgress(String, ...) */ -suspend fun Fragment.withProgress(@StringRes messageId: Int, block: suspend () -> T): T = - requireActivity().withProgress(messageId, block) +suspend fun Fragment.withProgress( + @StringRes messageId: Int, + block: suspend () -> T, +): T = requireActivity().withProgress(messageId, block) @Suppress("Deprecation") // ProgressDialog deprecation suspend fun withProgressDialog( context: Activity, onCancel: (() -> Unit)?, delayMillis: Long = 600, - op: suspend (android.app.ProgressDialog) -> T -): T = coroutineScope { - val dialog = android.app.ProgressDialog(context).apply { - setCancelable(onCancel != null) - onCancel?.let { - setOnCancelListener { it() } - } - } - // disable taps immediately - context.window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) - // reveal the dialog after 600ms - var dialogIsOurs = false - val dialogJob = launch { - delay(delayMillis) - if (!AnkiDroidApp.instance.progressDialogShown) { - dialog.show() - AnkiDroidApp.instance.progressDialogShown = true - dialogIsOurs = true - } - } - try { - op(dialog) - } finally { - dialogJob.cancel() - dialog.dismiss() - context.window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) - if (dialogIsOurs) { - AnkiDroidApp.instance.progressDialogShown = false + op: suspend (android.app.ProgressDialog) -> T, +): T = + coroutineScope { + val dialog = + android.app.ProgressDialog(context).apply { + setCancelable(onCancel != null) + onCancel?.let { + setOnCancelListener { it() } + } + } + // disable taps immediately + context.window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + // reveal the dialog after 600ms + var dialogIsOurs = false + val dialogJob = + launch { + delay(delayMillis) + if (!AnkiDroidApp.instance.progressDialogShown) { + dialog.show() + AnkiDroidApp.instance.progressDialogShown = true + dialogIsOurs = true + } + } + try { + op(dialog) + } finally { + dialogJob.cancel() + dialog.dismiss() + context.window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + if (dialogIsOurs) { + AnkiDroidApp.instance.progressDialogShown = false + } } } -} /** * Poll the backend for progress info every 100ms until cancelled by caller. @@ -328,7 +349,7 @@ suspend fun withProgressDialog( private suspend fun monitorProgress( backend: Backend, extractProgress: ProgressContext.() -> Unit, - updateUi: ProgressContext.() -> Unit + updateUi: ProgressContext.() -> Unit, ) { val state = ProgressContext(Progress.getDefaultInstance()) while (true) { @@ -349,7 +370,7 @@ data class ProgressContext( var progress: Progress, var text: String = "", /** If set, shows progress bar with a of b complete. */ - var amount: Pair? = null + var amount: Pair? = null, ) @Suppress("Deprecation") // ProgressDialog deprecation @@ -358,9 +379,10 @@ private fun ProgressContext.updateDialog(dialog: android.app.ProgressDialog) { // setting progress after starting with indeterminate progress, so we just use // this for now // this code has since been updated to ProgressDialog, and the above not rechecked - val progressText = amount?.let { - " ${it.first}/${it.second}" - } ?: "" + val progressText = + amount?.let { + " ${it.first}/${it.second}" + } ?: "" @Suppress("Deprecation") // ProgressDialog deprecation dialog.setMessage(text + progressText) } @@ -391,14 +413,15 @@ suspend fun AnkiActivity.userAcceptsSchemaChange(): Boolean { if (withCol { schemaChanged() }) { return true } - val hasAcceptedSchemaChange = suspendCoroutine { coroutine -> - AlertDialog.Builder(this).show { - message(text = TR.deckConfigWillRequireFullSync().replace("\\s+".toRegex(), " ")) - positiveButton(R.string.dialog_ok) { coroutine.resume(true) } - negativeButton(R.string.dialog_cancel) { coroutine.resume(false) } - setOnCancelListener { coroutine.resume(false) } + val hasAcceptedSchemaChange = + suspendCoroutine { coroutine -> + AlertDialog.Builder(this).show { + message(text = TR.deckConfigWillRequireFullSync().replace("\\s+".toRegex(), " ")) + positiveButton(R.string.dialog_ok) { coroutine.resume(true) } + negativeButton(R.string.dialog_cancel) { coroutine.resume(false) } + setOnCancelListener { coroutine.resume(false) } + } } - } if (hasAcceptedSchemaChange) { withCol { modSchemaNoCheck() } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt index 64d8208620e1..b0da571914be 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt @@ -39,7 +39,6 @@ import timber.log.Timber import java.util.* object CrashReportService { - // ACRA constants used for stored preferences const val FEEDBACK_REPORT_KEY = "reportErrorMode" const val FEEDBACK_REPORT_ASK = "2" @@ -48,16 +47,17 @@ object CrashReportService { /** Our ACRA configurations, initialized during Application.onCreate() */ @JvmStatic - private var logcatArgs = arrayOf( - "-t", - "500", - "-v", - "time", - "ActivityManager:I", - "SQLiteLog:W", - AnkiDroidApp.TAG + ":D", - "*:S" - ) + private var logcatArgs = + arrayOf( + "-t", + "500", + "-v", + "time", + "ActivityManager:I", + "SQLiteLog:W", + AnkiDroidApp.TAG + ":D", + "*:S", + ) @JvmStatic private var dialogEnabled = true @@ -73,74 +73,78 @@ object CrashReportService { private const val MIN_INTERVAL_MS = 60000 private const val EXCEPTION_MESSAGE = "Exception report sent by user manually. See: 'Comment/USER_COMMENT'" - private enum class ToastType(@StringRes private val toastMessageRes: Int) { + private enum class ToastType( + @StringRes private val toastMessageRes: Int, + ) { AUTO_TOAST(R.string.feedback_auto_toast_text), - MANUAL_TOAST(R.string.feedback_for_manual_toast_text); + MANUAL_TOAST(R.string.feedback_for_manual_toast_text), + ; fun getToastMessage(context: Context) = context.getString(toastMessageRes) } private fun createAcraCoreConfigBuilder(): CoreConfigurationBuilder { - val builder = CoreConfigurationBuilder() - .withBuildConfigClass(com.ichi2.anki.BuildConfig::class.java) // AnkiDroid BuildConfig - Acrarium#319 - .withExcludeMatchingSharedPreferencesKeys("username", "hkey") - .withSharedPreferencesName("acra") - .withReportContent( - ReportField.REPORT_ID, - ReportField.APP_VERSION_CODE, - ReportField.APP_VERSION_NAME, - ReportField.PACKAGE_NAME, - ReportField.FILE_PATH, - ReportField.PHONE_MODEL, - ReportField.ANDROID_VERSION, - ReportField.BUILD, - ReportField.BRAND, - ReportField.PRODUCT, - ReportField.TOTAL_MEM_SIZE, - ReportField.AVAILABLE_MEM_SIZE, - ReportField.BUILD_CONFIG, - ReportField.CUSTOM_DATA, - ReportField.STACK_TRACE, - ReportField.STACK_TRACE_HASH, - ReportField.CRASH_CONFIGURATION, - ReportField.USER_COMMENT, - ReportField.USER_APP_START_DATE, - ReportField.USER_CRASH_DATE, - ReportField.LOGCAT, - ReportField.INSTALLATION_ID, - ReportField.ENVIRONMENT, - ReportField.SHARED_PREFERENCES, - // ReportField.MEDIA_CODEC_LIST, - ReportField.THREAD_DETAILS - ) - .withLogcatArguments(*logcatArgs) - .withPluginConfigurations( - DialogConfigurationBuilder() - .withReportDialogClass(AnkiDroidCrashReportDialog::class.java) - .withCommentPrompt(mApplication.getString(R.string.empty_string)) - .withTitle(mApplication.getString(R.string.feedback_title)) - .withText(mApplication.getString(R.string.feedback_default_text)) - .withPositiveButtonText(mApplication.getString(R.string.feedback_report)) - .withResIcon(R.drawable.logo_star_144dp) - .withEnabled(dialogEnabled) - .build(), - HttpSenderConfigurationBuilder() - .withHttpMethod(HttpSender.Method.PUT) - .withUri(BuildConfig.ACRA_URL) - .withEnabled(true) - .build(), - ToastConfigurationBuilder() - .withText(toastText) - .withEnabled(true) - .build(), - LimiterConfigurationBuilder() - .withExceptionClassLimit(1000) - .withStacktraceLimit(1) - .withDeleteReportsOnAppUpdate(true) - .withResetLimitsOnAppUpdate(true) - .withEnabled(true) - .build() - ) + val builder = + CoreConfigurationBuilder() + .withBuildConfigClass(com.ichi2.anki.BuildConfig::class.java) // AnkiDroid BuildConfig - Acrarium#319 + .withExcludeMatchingSharedPreferencesKeys("username", "hkey") + .withSharedPreferencesName("acra") + .withReportContent( + ReportField.REPORT_ID, + ReportField.APP_VERSION_CODE, + ReportField.APP_VERSION_NAME, + ReportField.PACKAGE_NAME, + ReportField.FILE_PATH, + ReportField.PHONE_MODEL, + ReportField.ANDROID_VERSION, + ReportField.BUILD, + ReportField.BRAND, + ReportField.PRODUCT, + ReportField.TOTAL_MEM_SIZE, + ReportField.AVAILABLE_MEM_SIZE, + ReportField.BUILD_CONFIG, + ReportField.CUSTOM_DATA, + ReportField.STACK_TRACE, + ReportField.STACK_TRACE_HASH, + ReportField.CRASH_CONFIGURATION, + ReportField.USER_COMMENT, + ReportField.USER_APP_START_DATE, + ReportField.USER_CRASH_DATE, + ReportField.LOGCAT, + ReportField.INSTALLATION_ID, + ReportField.ENVIRONMENT, + ReportField.SHARED_PREFERENCES, + // ReportField.MEDIA_CODEC_LIST, + ReportField.THREAD_DETAILS, + ) + .withLogcatArguments(*logcatArgs) + .withPluginConfigurations( + DialogConfigurationBuilder() + .withReportDialogClass(AnkiDroidCrashReportDialog::class.java) + .withCommentPrompt(mApplication.getString(R.string.empty_string)) + .withTitle(mApplication.getString(R.string.feedback_title)) + .withText(mApplication.getString(R.string.feedback_default_text)) + .withPositiveButtonText(mApplication.getString(R.string.feedback_report)) + .withResIcon(R.drawable.logo_star_144dp) + .withEnabled(dialogEnabled) + .build(), + HttpSenderConfigurationBuilder() + .withHttpMethod(HttpSender.Method.PUT) + .withUri(BuildConfig.ACRA_URL) + .withEnabled(true) + .build(), + ToastConfigurationBuilder() + .withText(toastText) + .withEnabled(true) + .build(), + LimiterConfigurationBuilder() + .withExceptionClassLimit(1000) + .withStacktraceLimit(1) + .withDeleteReportsOnAppUpdate(true) + .withResetLimitsOnAppUpdate(true) + .withEnabled(true) + .build(), + ) ACRA.init(mApplication, builder) acraCoreConfigBuilder = builder fetchWebViewInformation().let { @@ -253,23 +257,39 @@ object CrashReportService { } /** Used when we don't have an exception to throw, but we know something is wrong and want to diagnose it */ - fun sendExceptionReport(message: String?, origin: String?) { + fun sendExceptionReport( + message: String?, + origin: String?, + ) { sendExceptionReport(ManuallyReportedException(message), origin, null) } - fun sendExceptionReport(e: Throwable, origin: String?) { + fun sendExceptionReport( + e: Throwable, + origin: String?, + ) { sendExceptionReport(e, origin, null) } - fun sendExceptionReport(e: Throwable, origin: String?, additionalInfo: String?) { + fun sendExceptionReport( + e: Throwable, + origin: String?, + additionalInfo: String?, + ) { sendExceptionReport(e, origin, additionalInfo, false) } - fun sendExceptionReport(e: Throwable, origin: String?, additionalInfo: String?, onlyIfSilent: Boolean) { + fun sendExceptionReport( + e: Throwable, + origin: String?, + additionalInfo: String?, + onlyIfSilent: Boolean, + ) { sendAnalyticsException(e, false) AnkiDroidApp.sentExceptionReportHack = true - val reportMode = mApplication.applicationContext.sharedPrefs() - .getString(FEEDBACK_REPORT_KEY, FEEDBACK_REPORT_ASK) + val reportMode = + mApplication.applicationContext.sharedPrefs() + .getString(FEEDBACK_REPORT_KEY, FEEDBACK_REPORT_ASK) if (onlyIfSilent) { if (FEEDBACK_REPORT_ALWAYS != reportMode) { Timber.i("sendExceptionReport - onlyIfSilent true, but ACRA is not 'always accept'. Skipping report send.") @@ -287,7 +307,10 @@ object CrashReportService { return ACRA.isACRASenderServiceProcess() } - fun isAcraEnabled(context: Context, defaultValue: Boolean): Boolean { + fun isAcraEnabled( + context: Context, + defaultValue: Boolean, + ): Boolean { if (!context.sharedPrefs().contains(ACRA.PREF_DISABLE_ACRA)) { // we shouldn't use defaultValue below, as it would be inverted which complicated understanding. Timber.w("No default value for '%s'", ACRA.PREF_DISABLE_ACRA) @@ -309,7 +332,10 @@ object CrashReportService { } } - fun onPreferenceChanged(ctx: Context, newValue: String) { + fun onPreferenceChanged( + ctx: Context, + newValue: String, + ) { setAcraReportingMode(newValue) // If the user changed error reporting, make sure future reports have a chance to post deleteACRALimiterData(ctx) @@ -345,7 +371,7 @@ object CrashReportService { deleteACRALimiterData(activity) sendExceptionReport( UserSubmittedException(EXCEPTION_MESSAGE), - "AnkiDroidApp.HelpDialog" + "AnkiDroidApp.HelpDialog", ) true } else { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CustomMaterialTapTargetPromptBuilder.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CustomMaterialTapTargetPromptBuilder.kt index ff24a6070be1..bebece7a9435 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CustomMaterialTapTargetPromptBuilder.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CustomMaterialTapTargetPromptBuilder.kt @@ -27,8 +27,9 @@ import uk.co.samuelwall.materialtaptargetprompt.extras.backgrounds.RectangleProm import uk.co.samuelwall.materialtaptargetprompt.extras.focals.CirclePromptFocal import uk.co.samuelwall.materialtaptargetprompt.extras.focals.RectanglePromptFocal -class CustomMaterialTapTargetPromptBuilder(val activity: Activity, private val featureIdentifier: T) : MaterialTapTargetPrompt.Builder(activity) where T : Enum, T : OnboardingFlag { - +class CustomMaterialTapTargetPromptBuilder(val activity: Activity, private val featureIdentifier: T) : MaterialTapTargetPrompt.Builder( + activity, +) where T : Enum, T : OnboardingFlag { private fun createRectangle(): CustomMaterialTapTargetPromptBuilder { promptFocal = RectanglePromptFocal() return this diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DatabaseCheck.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DatabaseCheck.kt index bf059e417803..098461dc10da 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DatabaseCheck.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DatabaseCheck.kt @@ -21,30 +21,33 @@ import com.ichi2.anki.CollectionManager.withCol fun DeckPicker.handleDatabaseCheck() { launchCatchingTask { - val problems = withProgress( - extractProgress = { - if (progress.hasDatabaseCheck()) { - progress.databaseCheck.let { - text = it.stage - amount = if (it.stageTotal > 0) { - Pair(it.stageCurrent, it.stageTotal) - } else { - null + val problems = + withProgress( + extractProgress = { + if (progress.hasDatabaseCheck()) { + progress.databaseCheck.let { + text = it.stage + amount = + if (it.stageTotal > 0) { + Pair(it.stageCurrent, it.stageTotal) + } else { + null + } } } + }, + onCancel = null, + ) { + withCol { + fixIntegrity() } - }, - onCancel = null - ) { - withCol { - fixIntegrity() } - } - val message = if (problems.isNotEmpty()) { - problems.joinToString("\n") - } else { - TR.databaseCheckRebuilt() - } + val message = + if (problems.isNotEmpty()) { + problems.joinToString("\n") + } else { + TR.databaseCheckRebuilt() + } showSimpleMessageDialog(message) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index e77ff627a1e5..fc03cdd36eeb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -1,4 +1,4 @@ -/**************************************************************************************** +/* * Copyright (c) 2009 Andrew Dubya * * Copyright (c) 2009 Nicolas Raoul * * Copyright (c) 2009 Edu Zamora * @@ -188,7 +188,9 @@ open class DeckPicker : @Suppress("Deprecation") // TODO: Encapsulate ProgressDialog within a class to limit the use of deprecated functionality var mProgressDialog: android.app.ProgressDialog? = null - private var mStudyoptionsFrame: View? = null // not lateInit - can be null + private var mStudyoptionsFrame: View? = null + + // not lateInit - can be null @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) lateinit var recyclerView: RecyclerView private lateinit var mRecyclerViewLayoutManager: LinearLayoutManager @@ -251,56 +253,64 @@ open class DeckPicker : override val permissionScreenLauncher = recreateActivityResultLauncher() - private val reviewLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - DeckPickerActivityResultCallback { - processReviewResults(it.resultCode) - } - ) + private val reviewLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + processReviewResults(it.resultCode) + }, + ) - private val showNewVersionInfoLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - DeckPickerActivityResultCallback { - showStartupScreensAndDialogs(baseContext.sharedPrefs(), 3) - } - ) + private val showNewVersionInfoLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + showStartupScreensAndDialogs(baseContext.sharedPrefs(), 3) + }, + ) - private val loginForSyncLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - DeckPickerActivityResultCallback { - if (it.resultCode == RESULT_OK) { - mSyncOnResume = true - } - } - ) + private val loginForSyncLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + if (it.resultCode == RESULT_OK) { + mSyncOnResume = true + } + }, + ) - private val requestPathUpdateLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - DeckPickerActivityResultCallback { - // The collection path was inaccessible on startup so just close the activity and let user restart - finish() - } - ) + private val requestPathUpdateLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + // The collection path was inaccessible on startup so just close the activity and let user restart + finish() + }, + ) - private val apkgFileImportResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - DeckPickerActivityResultCallback { - if (it.resultCode == RESULT_OK) { - onSelectedPackageToImport(it.data!!) - } - } - ) + private val apkgFileImportResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + if (it.resultCode == RESULT_OK) { + onSelectedPackageToImport(it.data!!) + } + }, + ) - private val csvImportResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - DeckPickerActivityResultCallback { - if (it.resultCode == RESULT_OK) { - onSelectedCsvForImport(it.data!!) - } - } - ) + private val csvImportResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + if (it.resultCode == RESULT_OK) { + onSelectedCsvForImport(it.data!!) + } + }, + ) - private inner class DeckPickerActivityResultCallback(private val callback: (result: ActivityResult) -> Unit) : ActivityResultCallback { + private inner class DeckPickerActivityResultCallback( + private val callback: (result: ActivityResult) -> Unit, + ) : ActivityResultCallback { override fun onActivityResult(result: ActivityResult) { if (result.resultCode == RESULT_MEDIA_EJECTED) { onSdCardNotMounted() @@ -327,12 +337,17 @@ open class DeckPicker : // ---------------------------------------------------------------------------- // LISTENERS // ---------------------------------------------------------------------------- - private val mDeckExpanderClickListener = View.OnClickListener { view: View -> - launchCatchingTask { toggleDeckExpand(view.tag as Long) } - } + private val mDeckExpanderClickListener = + View.OnClickListener { view: View -> + launchCatchingTask { toggleDeckExpand(view.tag as Long) } + } private val mDeckClickListener = View.OnClickListener { v: View -> onDeckClick(v, DeckSelectionType.DEFAULT) } private val mCountsClickListener = View.OnClickListener { v: View -> onDeckClick(v, DeckSelectionType.SHOW_STUDY_OPTIONS) } - private fun onDeckClick(v: View, selectionType: DeckSelectionType) { + + private fun onDeckClick( + v: View, + selectionType: DeckSelectionType, + ) { val deckId = v.tag as Long Timber.i("DeckPicker:: Selected deck with id %d", deckId) launchCatchingTask { @@ -346,16 +361,18 @@ open class DeckPicker : } } - private val mDeckLongClickListener = OnLongClickListener { v -> - val deckId = v.tag as Long - Timber.i("DeckPicker:: Long tapped on deck with id %d", deckId) - showDialogFragment(mContextMenuFactory.newDeckPickerContextMenu(deckId)) - true - } + private val mDeckLongClickListener = + OnLongClickListener { v -> + val deckId = v.tag as Long + Timber.i("DeckPicker:: Long tapped on deck with id %d", deckId) + showDialogFragment(mContextMenuFactory.newDeckPickerContextMenu(deckId)) + true + } // ---------------------------------------------------------------------------- // ANDROID ACTIVITY METHODS // ---------------------------------------------------------------------------- + /** Called when the activity is first created. */ @Throws(SQLException::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -427,7 +444,8 @@ open class DeckPicker : var hasDeckPickerBackground = false try { hasDeckPickerBackground = applyDeckPickerBackground() - } catch (e: OutOfMemoryError) { // 6608 - OOM should be catchable here. + } catch (e: OutOfMemoryError) { + // 6608 - OOM should be catchable here. Timber.w(e, "Failed to apply background - OOM") showThemedToast(this, getString(R.string.background_image_too_large), false) } catch (e: Exception) { @@ -437,26 +455,28 @@ open class DeckPicker : mExportingDelegate.onRestoreInstanceState(savedInstanceState) // create and set an adapter for the RecyclerView - mDeckListAdapter = DeckAdapter(layoutInflater, this).apply { - setDeckClickListener(mDeckClickListener) - setCountsClickListener(mCountsClickListener) - setDeckExpanderClickListener(mDeckExpanderClickListener) - setDeckLongClickListener(mDeckLongClickListener) - enablePartialTransparencyForBackground(hasDeckPickerBackground) - } + mDeckListAdapter = + DeckAdapter(layoutInflater, this).apply { + setDeckClickListener(mDeckClickListener) + setCountsClickListener(mCountsClickListener) + setDeckExpanderClickListener(mDeckExpanderClickListener) + setDeckLongClickListener(mDeckLongClickListener) + enablePartialTransparencyForBackground(hasDeckPickerBackground) + } recyclerView.adapter = mDeckListAdapter - mPullToSyncWrapper = findViewById(R.id.pull_to_sync_wrapper).apply { - setDistanceToTriggerSync(SWIPE_TO_SYNC_TRIGGER_DISTANCE) - setOnRefreshListener { - Timber.i("Pull to Sync: Syncing") - mPullToSyncWrapper.isRefreshing = false - sync() - } - viewTreeObserver.addOnScrollChangedListener { - mPullToSyncWrapper.isEnabled = mRecyclerViewLayoutManager.findFirstCompletelyVisibleItemPosition() == 0 + mPullToSyncWrapper = + findViewById(R.id.pull_to_sync_wrapper).apply { + setDistanceToTriggerSync(SWIPE_TO_SYNC_TRIGGER_DISTANCE) + setOnRefreshListener { + Timber.i("Pull to Sync: Syncing") + mPullToSyncWrapper.isRefreshing = false + sync() + } + viewTreeObserver.addOnScrollChangedListener { + mPullToSyncWrapper.isEnabled = mRecyclerViewLayoutManager.findFirstCompletelyVisibleItemPosition() == 0 + } } - } // Setup the FloatingActionButtons, should work everywhere with min API >= 15 mFloatingActionMenu = DeckPickerFloatingActionMenu(this, view, this) @@ -560,16 +580,17 @@ open class DeckPicker : Timber.d("handleStartup: Continuing. unaffected by storage migration") val failure = InitialActivity.getStartupFailureType(this) - mStartupError = if (failure == null) { - // Show any necessary dialogs (e.g. changelog, special messages, etc) - val sharedPrefs = this.sharedPrefs() - showStartupScreensAndDialogs(sharedPrefs, 0) - false - } else { - // Show error dialogs - handleStartupFailure(failure) - true - } + mStartupError = + if (failure == null) { + // Show any necessary dialogs (e.g. changelog, special messages, etc) + val sharedPrefs = this.sharedPrefs() + showStartupScreensAndDialogs(sharedPrefs, 0) + false + } else { + // Show error dialogs + handleStartupFailure(failure) + true + } } @VisibleForTesting @@ -597,19 +618,21 @@ open class DeckPicker : Timber.i("Displaying database locked error") showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_DB_LOCKED) } - 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 + 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) { - exit() + positiveButton(R.string.close) { + exit() + } + cancelable(false) } - cancelable(false) - } DISK_FULL -> displayNoStorageError() DB_ERROR -> displayDatabaseFailure() else -> displayDatabaseFailure() @@ -620,6 +643,7 @@ open class DeckPicker : Timber.i("Displaying database failure") showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_LOAD_FAILED) } + private fun displayNoStorageError() { Timber.i("Displaying no storage error") showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_DISK_FULL) @@ -661,10 +685,11 @@ open class DeckPicker : // Store the job so that tests can easily await it. In the future // this may be better done by injecting a custom test scheduler // into CollectionManager, and awaiting that. - createMenuJob = launchCatchingTask { - updateMenuState() - updateMenuFromState(menu) - } + createMenuJob = + launchCatchingTask { + updateMenuState() + updateMenuFromState(menu) + } return super.onCreateOptionsMenu(menu) } @@ -692,9 +717,13 @@ 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, mediaMigrationState: MediaMigrationState) { - val migrationProgressMenuItem = menu.findItem(R.id.action_migration_progress) - .apply { isVisible = mediaMigrationState is MediaMigrationState.Ongoing.NotPaused } + private fun setupMigrationProgressMenuItem( + menu: Menu, + mediaMigrationState: MediaMigrationState, + ) { + val migrationProgressMenuItem = + menu.findItem(R.id.action_migration_progress) + .apply { isVisible = mediaMigrationState is MediaMigrationState.Ongoing.NotPaused } fun CircularProgressIndicator.publishProgress(progress: MigrationService.Progress.MovingMediaFiles) { when (progress) { @@ -711,39 +740,42 @@ open class DeckPicker : if (mediaMigrationState is MediaMigrationState.Ongoing.NotPaused) { if (cachedMigrationProgressMenuItemActionView == null) { - val actionView = migrationProgressMenuItem.actionView!! - .also { cachedMigrationProgressMenuItemActionView = it } + val actionView = + migrationProgressMenuItem.actionView!! + .also { cachedMigrationProgressMenuItemActionView = it } - val progressIndicator = actionView - .findViewById(R.id.progress_indicator) - .apply { max = Int.MAX_VALUE } + val progressIndicator = + actionView + .findViewById(R.id.progress_indicator) + .apply { max = Int.MAX_VALUE } actionView.findViewById(R.id.button).also { button -> button.setOnClickListener { warnNoSyncDuringMigration() } TooltipCompat.setTooltipText(button, getText(R.string.show_migration_progress)) } - migrationProgressPublishingJob = lifecycleScope.launch { - MigrationService.flowOfProgress - .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) - .filterNotNull() - .collect { progress -> - when (progress) { - is MigrationService.Progress.CopyingEssentialFiles -> { - // Button is not shown when transferring essential files - } - - is MigrationService.Progress.MovingMediaFiles -> { - progressIndicator.publishProgress(progress) - } - - is MigrationService.Progress.Done -> { - updateMenuState() - updateMenuFromState(menu) + migrationProgressPublishingJob = + lifecycleScope.launch { + MigrationService.flowOfProgress + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .filterNotNull() + .collect { progress -> + when (progress) { + is MigrationService.Progress.CopyingEssentialFiles -> { + // Button is not shown when transferring essential files + } + + is MigrationService.Progress.MovingMediaFiles -> { + progressIndicator.publishProgress(progress) + } + + is MigrationService.Progress.Done -> { + updateMenuState() + updateMenuFromState(menu) + } } } - } - } + } } else { migrationProgressMenuItem.actionView = cachedMigrationProgressMenuItemActionView } @@ -756,38 +788,42 @@ open class DeckPicker : } private fun setupSearchIcon(menuItem: MenuItem) { - menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { - // When SearchItem is expanded - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - Timber.i("DeckPicker:: SearchItem opened") - // Hide the floating action button if it is visible - mFloatingActionMenu.hideFloatingActionButton() - return true - } - - // When SearchItem is collapsed - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - Timber.i("DeckPicker:: SearchItem closed") - // Show the floating action button if it is hidden - mFloatingActionMenu.showFloatingActionButton() - return true - } - }) - - (menuItem.actionView as SearchView).run { - queryHint = getString(R.string.search_decks) - setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - clearFocus() + menuItem.setOnActionExpandListener( + object : MenuItem.OnActionExpandListener { + // When SearchItem is expanded + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + Timber.i("DeckPicker:: SearchItem opened") + // Hide the floating action button if it is visible + mFloatingActionMenu.hideFloatingActionButton() return true } - override fun onQueryTextChange(newText: String): Boolean { - val adapter = recyclerView.adapter as Filterable? - adapter!!.filter.filter(newText) + // When SearchItem is collapsed + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + Timber.i("DeckPicker:: SearchItem closed") + // Show the floating action button if it is hidden + mFloatingActionMenu.showFloatingActionButton() return true } - }) + }, + ) + + (menuItem.actionView as SearchView).run { + queryHint = getString(R.string.search_decks) + setOnQueryTextListener( + object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + clearFocus() + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + val adapter = recyclerView.adapter as Filterable? + adapter!!.filter.filter(newText) + return true + } + }, + ) } searchDecksIcon = menuItem } @@ -803,7 +839,10 @@ open class DeckPicker : } } - private fun updateUndoLabelFromState(menuItem: MenuItem, undoLabel: String?) { + private fun updateUndoLabelFromState( + menuItem: MenuItem, + undoLabel: String?, + ) { menuItem.run { if (undoLabel != null) { isVisible = true @@ -814,7 +853,10 @@ open class DeckPicker : } } - private fun updateSyncIconFromState(menuItem: MenuItem, state: OptionsMenuState) { + private fun updateSyncIconFromState( + menuItem: MenuItem, + state: OptionsMenuState, + ) { if (state.mediaMigrationState is MediaMigrationState.Ongoing) { menuItem.isVisible = false } else { @@ -825,7 +867,7 @@ open class DeckPicker : 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) { @@ -849,17 +891,19 @@ open class DeckPicker : @VisibleForTesting suspend fun updateMenuState() { - optionsMenuState = withOpenColOrNull { - val searchIcon = decks.count() >= 10 - val undoLabel = undoLabel() - Pair(searchIcon, undoLabel) - }?.let { (searchIcon, undoLabel) -> - val syncIcon = fetchSyncStatus() - val mediaMigrationState = getMediaMigrationState() - val shouldShowStartMigrationButton = shouldOfferToMigrate() || - mediaMigrationState is MediaMigrationState.Ongoing.PausedDueToError - OptionsMenuState(searchIcon, undoLabel, syncIcon, shouldShowStartMigrationButton, mediaMigrationState) - } + optionsMenuState = + withOpenColOrNull { + val searchIcon = decks.count() >= 10 + val undoLabel = undoLabel() + Pair(searchIcon, undoLabel) + }?.let { (searchIcon, undoLabel) -> + val syncIcon = fetchSyncStatus() + val mediaMigrationState = getMediaMigrationState() + val shouldShowStartMigrationButton = + shouldOfferToMigrate() || + mediaMigrationState is MediaMigrationState.Ongoing.PausedDueToError + OptionsMenuState(searchIcon, undoLabel, syncIcon, shouldShowStartMigrationButton, mediaMigrationState) + } } // TODO BEFORE-RELEASE This doesn't offer to migrate data if not logged in. @@ -957,7 +1001,8 @@ open class DeckPicker : } fun createFilteredDialog() { - val createFilteredDeckDialog = CreateDeckDialog(this@DeckPicker, R.string.new_deck, CreateDeckDialog.DeckDialogType.FILTERED_DECK, null) + val createFilteredDeckDialog = + CreateDeckDialog(this@DeckPicker, R.string.new_deck, CreateDeckDialog.DeckDialogType.FILTERED_DECK, null) createFilteredDeckDialog.setOnNewDeckCreated { // a filtered deck was created openFilteredDeckOptions() @@ -974,8 +1019,8 @@ open class DeckPicker : ExportDialogParams( message = resources.getString(R.string.confirm_apkg_export), exportType = ExportType.ExportCollection, - includeMedia = includeMedia - ) + includeMedia = includeMedia, + ), ) } @@ -1072,12 +1117,15 @@ open class DeckPicker : val automaticSyncIntervalInMS = AUTOMATIC_SYNC_MINIMAL_INTERVAL_IN_MINUTES * 60 * 1000 val syncIntervalPassed = TimeManager.time.intTimeMS() - lastSyncTime > automaticSyncIntervalInMS - val isNotBlockedByMeteredConnection = preferences.getBoolean( - getString(R.string.metered_sync_key), - false - ) || !isActiveNetworkMetered() + val isNotBlockedByMeteredConnection = + preferences.getBoolean( + getString(R.string.metered_sync_key), + false, + ) || !isActiveNetworkMetered() val isMigratingStorage = mediaMigrationIsInProgress(this) - if (isLoggedIn() && autoSyncIsEnabled && NetworkUtils.isOnline && syncIntervalPassed && isNotBlockedByMeteredConnection && !isMigratingStorage) { + if (isLoggedIn() && autoSyncIsEnabled && NetworkUtils.isOnline && syncIntervalPassed && + isNotBlockedByMeteredConnection && !isMigratingStorage + ) { Timber.i("Triggering Automatic Sync") sync() } @@ -1096,7 +1144,7 @@ open class DeckPicker : } else { if (!preferences.getBoolean( "exitViaDoubleTapBack", - false + false, ) || mBackButtonPressedToExit ) { automaticSync() @@ -1112,7 +1160,10 @@ open class DeckPicker : } } - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { if (mToolbarSearchView != null && mToolbarSearchView!!.hasFocus()) { Timber.d("Skipping keypress: search action bar is focused") return true @@ -1171,11 +1222,12 @@ open class DeckPicker : // If libanki determines it's necessary to confirm the full sync then show a confirmation dialog // We have to show the dialog via the DialogHandler since this method is called via an async task val res = resources - val message = """ - ${res.getString(R.string.full_sync_confirmation_upgrade)} - - ${res.getString(R.string.full_sync_confirmation)} - """.trimIndent() + val message = + """ + ${res.getString(R.string.full_sync_confirmation_upgrade)} + + ${res.getString(R.string.full_sync_confirmation)} + """.trimIndent() dialogHandler.sendMessage(ForceFullSyncDialog(message).toMessage()) } @@ -1205,7 +1257,10 @@ open class DeckPicker : startActivity(intent) } - private fun showStartupScreensAndDialogs(preferences: SharedPreferences, skip: Int) { + private fun showStartupScreensAndDialogs( + preferences: SharedPreferences, + skip: Int, + ) { // For Android 8/8.1 we want to use software rendering by default or the Reviewer UI is broken #7369 if (sdkVersion == Build.VERSION_CODES.O || sdkVersion == Build.VERSION_CODES.O_MR1 @@ -1239,13 +1294,14 @@ open class DeckPicker : // installation of AnkiDroid and we don't run the check. val current = VersionUtils.pkgVersionCode Timber.i("Current AnkiDroid version: %s", current) - val previous: Long = if (preferences.contains(UPGRADE_VERSION_KEY)) { - // Upgrading currently installed app - getPreviousVersion(preferences, current) - } else { - // Fresh install - current - } + val previous: Long = + if (preferences.contains(UPGRADE_VERSION_KEY)) { + // Upgrading currently installed app + getPreviousVersion(preferences, current) + } else { + // Fresh install + current + } preferences.edit { putLong(UPGRADE_VERSION_KEY, current) } // Delete the media database made by any version before 2.3 beta due to upgrade errors. // It is rebuilt on the next sync or media check @@ -1351,27 +1407,31 @@ open class DeckPicker : showDialogFragment(DeckPickerAnalyticsOptInDialog.newInstance()) } - fun getPreviousVersion(preferences: SharedPreferences, current: Long): Long { + fun getPreviousVersion( + preferences: SharedPreferences, + current: Long, + ): Long { var previous: Long try { previous = preferences.getLong(UPGRADE_VERSION_KEY, current) } catch (e: ClassCastException) { Timber.w(e) - previous = try { - // set 20900203 to default value, as it's the latest version that stores integer in shared prefs - preferences.getInt(UPGRADE_VERSION_KEY, 20900203).toLong() - } catch (cce: ClassCastException) { - Timber.w(cce) - // Previous versions stored this as a string. - val s = preferences.getString(UPGRADE_VERSION_KEY, "") - // The last version of AnkiDroid that stored this as a string was 2.0.2. - // We manually set the version here, but anything older will force a DB check. - if ("2.0.2" == s) { - 40 - } else { - 0 + previous = + try { + // set 20900203 to default value, as it's the latest version that stores integer in shared prefs + preferences.getInt(UPGRADE_VERSION_KEY, 20900203).toLong() + } catch (cce: ClassCastException) { + Timber.w(cce) + // Previous versions stored this as a string. + val s = preferences.getString(UPGRADE_VERSION_KEY, "") + // The last version of AnkiDroid that stored this as a string was 2.0.2. + // We manually set the version here, but anything older will force a DB check. + if ("2.0.2" == s) { + 40 + } else { + 0 + } } - } Timber.d("Updating shared preferences stored key %s type to long", UPGRADE_VERSION_KEY) // Expected Editor.putLong to be called later to update the value in shared prefs preferences.edit().remove(UPGRADE_VERSION_KEY).apply() @@ -1404,7 +1464,10 @@ open class DeckPicker : showAsyncDialogFragment(MediaCheckDialog.newInstance(dialogType)) } - override fun showMediaCheckDialog(dialogType: Int, checkList: MediaCheckResult) { + override fun showMediaCheckDialog( + dialogType: Int, + checkList: MediaCheckResult, + ) { showAsyncDialogFragment(MediaCheckDialog.newInstance(dialogType, checkList)) } @@ -1421,7 +1484,10 @@ open class DeckPicker : * @param dialogType id of dialog to show * @param message text to show */ - override fun showSyncErrorDialog(dialogType: Int, message: String?) { + override fun showSyncErrorDialog( + dialogType: Int, + message: String?, + ) { val newFragment: AsyncDialogFragment = newInstance(dialogType, message) showAsyncDialogFragment(newFragment, Channel.SYNC) } @@ -1442,13 +1508,14 @@ open class DeckPicker : // TODO: doesn't work on null collection-only on non-openable(is this still relevant with withCol?) launchCatchingTask(resources.getString(R.string.deck_repair_error)) { Timber.d("doInBackgroundRepairCollection") - val result = withProgress(resources.getString(R.string.backup_repair_deck_progress)) { - withCol { - Timber.i("RepairCollection: Closing collection") - close() - BackupManager.repairCollection(this) + val result = + withProgress(resources.getString(R.string.backup_repair_deck_progress)) { + withCol { + Timber.i("RepairCollection: Closing collection") + close() + BackupManager.repairCollection(this) + } } - } if (!result) { showThemedToast(this@DeckPicker, resources.getString(R.string.deck_repair_error), true) showCollectionErrorDialog() @@ -1504,12 +1571,13 @@ open class DeckPicker : override fun deleteUnused(unused: List) { launchCatchingTask { // Number of deleted files - val noOfDeletedFiles = withProgress(resources.getString(R.string.delete_media_message)) { - withCol { deleteMedia(this, unused) } - } + val noOfDeletedFiles = + withProgress(resources.getString(R.string.delete_media_message)) { + withCol { deleteMedia(this, unused) } + } showSimpleMessageDialog( title = resources.getString(R.string.delete_media_result_title), - message = resources.getQuantityString(R.plurals.delete_media_result_message, noOfDeletedFiles, noOfDeletedFiles) + message = resources.getQuantityString(R.plurals.delete_media_result_message, noOfDeletedFiles, noOfDeletedFiles), ) } } @@ -1585,7 +1653,7 @@ open class DeckPicker : preferences.edit { putBoolean( getString(R.string.metered_sync_key), - isCheckboxChecked + isCheckboxChecked, ) } } @@ -1641,15 +1709,19 @@ open class DeckPicker : */ private fun registerExternalStorageListener() { if (mUnmountReceiver == null) { - mUnmountReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == SdCardReceiver.MEDIA_EJECT) { - onSdCardNotMounted() - } else if (intent.action == SdCardReceiver.MEDIA_MOUNT) { - ActivityCompat.recreate(this@DeckPicker) + mUnmountReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (intent.action == SdCardReceiver.MEDIA_EJECT) { + onSdCardNotMounted() + } else if (intent.action == SdCardReceiver.MEDIA_MOUNT) { + ActivityCompat.recreate(this@DeckPicker) + } } } - } val iFilter = IntentFilter() iFilter.addAction(SdCardReceiver.MEDIA_EJECT) iFilter.addAction(SdCardReceiver.MEDIA_MOUNT) @@ -1668,7 +1740,9 @@ open class DeckPicker : startActivity(intent) } - private fun openStudyOptions(@Suppress("SameParameterValue") withDeckOptions: Boolean) { + private fun openStudyOptions( + @Suppress("SameParameterValue") withDeckOptions: Boolean, + ) { if (fragmented) { // The fragment will show the study options screen instead of launching a new activity. loadStudyOptionsFragment(withDeckOptions) @@ -1725,11 +1799,15 @@ open class DeckPicker : } @NeedsTest("14608: Ensure that the deck options refer to the selected deck") - private suspend fun handleDeckSelection(did: DeckId, selectionType: DeckSelectionType) { - fun showEmptyDeckSnackbar() = showSnackbar(R.string.empty_deck) { - addCallback(mSnackbarShowHideCallback) - setAction(R.string.menu_add) { addNote() } - } + private suspend fun handleDeckSelection( + did: DeckId, + selectionType: DeckSelectionType, + ) { + fun showEmptyDeckSnackbar() = + showSnackbar(R.string.empty_deck) { + addCallback(mSnackbarShowHideCallback) + setAction(R.string.menu_add) { addNote() } + } /** Check if we need to update the fragment or update the deck list */ fun updateUi() { @@ -1763,7 +1841,8 @@ open class DeckPicker : CompletedDeckStatus.LEARN_AHEAD_LIMIT_REACHED, CompletedDeckStatus.REGULAR_DECK_NO_MORE_CARDS_TODAY, CompletedDeckStatus.DYNAMIC_DECK_NO_LIMITS_REACHED, - CompletedDeckStatus.DAILY_STUDY_LIMIT_REACHED -> { + CompletedDeckStatus.DAILY_STUDY_LIMIT_REACHED, + -> { startActivity(CongratsPage.getIntent(this)) } CompletedDeckStatus.EMPTY_REGULAR_DECK -> { @@ -1804,18 +1883,23 @@ open class DeckPicker : } Timber.d("updateDeckList") loadDeckCounts?.cancel() - loadDeckCounts = launchCatchingTask { - withProgress { - Timber.d("Refreshing deck list") - val deckData = withCol { - Pair(sched.deckDueTree(), this.isEmpty) + loadDeckCounts = + launchCatchingTask { + withProgress { + Timber.d("Refreshing deck list") + val deckData = + withCol { + Pair(sched.deckDueTree(), this.isEmpty) + } + onDecksLoaded(deckData.first, deckData.second) } - onDecksLoaded(deckData.first, deckData.second) } - } } - private fun onDecksLoaded(result: DeckNode, collectionIsEmpty: Boolean) { + private fun onDecksLoaded( + result: DeckNode, + collectionIsEmpty: Boolean, + ) { Timber.i("Updating deck list UI") hideProgressBar() // Make sure the fragment is visible @@ -1827,9 +1911,10 @@ open class DeckPicker : // Update the mini statistics bar as well mReviewSummaryTextView.setSingleLine() launchCatchingTask { - mReviewSummaryTextView.text = withCol { - sched.studiedToday() - } + mReviewSummaryTextView.text = + withCol { + sched.studiedToday() + } } Timber.d("Startup - Deck List UI Completed") } @@ -1850,11 +1935,12 @@ open class DeckPicker : mDeckPickerContent.visibility = if (isEmpty) View.GONE else View.VISIBLE mNoDecksPlaceholder.visibility = if (isEmpty) View.VISIBLE else View.GONE } else { - val translation = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 8f, - resources.displayMetrics - ) + val translation = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + resources.displayMetrics, + ) val decksListShown = mDeckPickerContent.visibility == View.VISIBLE val placeholderShown = mNoDecksPlaceholder.visibility == View.VISIBLE if (isEmpty) { @@ -1862,11 +1948,13 @@ open class DeckPicker : fadeOut(mDeckPickerContent, mShortAnimDuration, translation) } if (!placeholderShown) { - fadeIn(mNoDecksPlaceholder, mShortAnimDuration, translation).startDelay = if (decksListShown) mShortAnimDuration * 2.toLong() else 0.toLong() + fadeIn(mNoDecksPlaceholder, mShortAnimDuration, translation).startDelay = + if (decksListShown) mShortAnimDuration * 2.toLong() else 0.toLong() } } else { if (!decksListShown) { - fadeIn(mDeckPickerContent, mShortAnimDuration, translation).startDelay = if (placeholderShown) mShortAnimDuration * 2.toLong() else 0.toLong() + fadeIn(mDeckPickerContent, mShortAnimDuration, translation).startDelay = + if (placeholderShown) mShortAnimDuration * 2.toLong() else 0.toLong() } if (placeholderShown) { fadeOut(mNoDecksPlaceholder, mShortAnimDuration, translation) @@ -1895,11 +1983,12 @@ open class DeckPicker : if (due != null && supportActionBar != null) { val cardCount = withCol { cardCount() } - val subTitle: String = if (due == 0) { - res.getQuantityString(R.plurals.deckpicker_title_zero_due, cardCount, cardCount) - } else { - res.getQuantityString(R.plurals.widget_cards_due, due, due) - } + val subTitle: String = + if (due == 0) { + res.getQuantityString(R.plurals.deckpicker_title_zero_due, cardCount, cardCount) + } else { + res.getQuantityString(R.plurals.widget_cards_due, due, due) + } supportActionBar!!.subtitle = subTitle } } catch (e: RuntimeException) { @@ -1931,23 +2020,27 @@ open class DeckPicker : mExportingDelegate.showExportDialog( ExportDialogParams( message = resources.getString(R.string.confirm_apkg_export_deck, getColUnsafe.decks.name(did)), - exportType = ExportType.ExportDeck(did) - ) + exportType = ExportType.ExportDeck(did), + ), ) } - fun createIcon(context: Context, did: DeckId) { + fun createIcon( + context: Context, + did: DeckId, + ) { // This code should not be reachable with lower versions - val shortcut = ShortcutInfoCompat.Builder(this, did.toString()) - .setIntent( - Intent(context, Reviewer::class.java) - .setAction(Intent.ACTION_VIEW) - .putExtra("deckId", did) - ) - .setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher)) - .setShortLabel(Decks.basename(getColUnsafe.decks.name(did))) - .setLongLabel(getColUnsafe.decks.name(did)) - .build() + val shortcut = + ShortcutInfoCompat.Builder(this, did.toString()) + .setIntent( + Intent(context, Reviewer::class.java) + .setAction(Intent.ACTION_VIEW) + .putExtra("deckId", did), + ) + .setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher)) + .setShortLabel(Decks.basename(getColUnsafe.decks.name(did))) + .setLongLabel(getColUnsafe.decks.name(did)) + .build() try { val success = ShortcutManagerCompat.requestPinShortcut(this, shortcut, null) @@ -2000,11 +2093,12 @@ open class DeckPicker : */ fun deleteDeck(did: DeckId): Job { return launchCatchingTask { - val changes = withProgress(resources.getString(R.string.delete_deck)) { - undoableOp { - decks.removeDecks(listOf(did)) + val changes = + withProgress(resources.getString(R.string.delete_deck)) { + undoableOp { + decks.removeDecks(listOf(did)) + } } - } showSnackbar(TR.browsingCardsDeleted(changes.count), Snackbar.LENGTH_SHORT) } } @@ -2070,11 +2164,12 @@ open class DeckPicker : private fun handleEmptyCards() { launchCatchingTask { - val emptyCids = withProgress(R.string.emtpy_cards_finding) { - withCol { - emptyCids() + val emptyCids = + withProgress(R.string.emtpy_cards_finding) { + withCol { + emptyCids() + } } - } AlertDialog.Builder(this@DeckPicker).show { setTitle(TR.emptyCardsWindowTitle()) if (emptyCids.isEmpty()) { @@ -2133,7 +2228,7 @@ open class DeckPicker : SHOW_STUDY_OPTIONS, /** Always open reviewer (keyboard shortcut) */ - SKIP_STUDY_OPTIONS + SKIP_STUDY_OPTIONS, } companion object { @@ -2164,7 +2259,12 @@ open class DeckPicker : private const val SWIPE_TO_SYNC_TRIGGER_DISTANCE = 400 // Animation utility methods used by renderPage() method - fun fadeIn(view: View?, duration: Int, translation: Float = 0f, startAction: Runnable? = Runnable { view!!.visibility = View.VISIBLE }): ViewPropertyAnimator { + fun fadeIn( + view: View?, + duration: Int, + translation: Float = 0f, + startAction: Runnable? = Runnable { view!!.visibility = View.VISIBLE }, + ): ViewPropertyAnimator { view!!.alpha = 0f view.translationY = translation return view.animate() @@ -2174,7 +2274,15 @@ open class DeckPicker : .withStartAction(startAction) } - fun fadeOut(view: View?, duration: Int, translation: Float = 0f, endAction: Runnable? = Runnable { view!!.visibility = View.GONE }): ViewPropertyAnimator { + fun fadeOut( + view: View?, + duration: Int, + translation: Float = 0f, + endAction: Runnable? = + Runnable { + view!!.visibility = View.GONE + }, + ): ViewPropertyAnimator { view!!.alpha = 1f view.translationY = 0f return view.animate() @@ -2185,7 +2293,10 @@ open class DeckPicker : } } - override fun opExecuted(changes: OpChanges, handler: Any?) { + override fun opExecuted( + changes: OpChanges, + handler: Any?, + ) { if (changes.studyQueues && handler !== this) { invalidateOptionsMenu() updateDeckList() @@ -2217,26 +2328,28 @@ open class DeckPicker : } @OptIn(ExperimentalTime::class) - private fun launchShowingHidingEssentialFileMigrationProgressDialog() = lifecycleScope.launch { - while (true) { - MigrationService.flowOfProgress - .first { it is MigrationService.Progress.CopyingEssentialFiles } - - val (progress, duration) = measureTimedValue { - withImmediatelyShownProgress(R.string.start_migration_progress_message) { - MigrationService.flowOfProgress - .first { it !is MigrationService.Progress.CopyingEssentialFiles } + private fun launchShowingHidingEssentialFileMigrationProgressDialog() = + lifecycleScope.launch { + while (true) { + MigrationService.flowOfProgress + .first { it is MigrationService.Progress.CopyingEssentialFiles } + + val (progress, duration) = + measureTimedValue { + withImmediatelyShownProgress(R.string.start_migration_progress_message) { + MigrationService.flowOfProgress + .first { it !is MigrationService.Progress.CopyingEssentialFiles } + } + } + + if (progress is MigrationService.Progress.MovingMediaFiles && duration > 800.milliseconds) { + showSnackbar(R.string.migration_part_1_done_resume) } - } - if (progress is MigrationService.Progress.MovingMediaFiles && duration > 800.milliseconds) { - showSnackbar(R.string.migration_part_1_done_resume) + refreshState() + updateDeckList() } - - refreshState() - updateDeckList() } - } /** * Show a dialog that explains no sync can occur during migration. @@ -2250,16 +2363,18 @@ open class DeckPicker : */ private var migrationWasLastPostponedAt: Long get() = baseContext.sharedPrefs().getLong(MIGRATION_WAS_LAST_POSTPONED_AT_SECONDS, 0L) - set(timeInSecond) = baseContext.sharedPrefs() - .edit { putLong(MIGRATION_WAS_LAST_POSTPONED_AT_SECONDS, timeInSecond) } + set(timeInSecond) = + baseContext.sharedPrefs() + .edit { putLong(MIGRATION_WAS_LAST_POSTPONED_AT_SECONDS, timeInSecond) } /** * The number of times the storage migration was postponed. -1 for 'disabled' */ private var timesStorageMigrationPostponed: Int get() = baseContext.sharedPrefs().getInt(TIMES_STORAGE_MIGRATION_POSTPONED_KEY, 0) - set(value) = baseContext.sharedPrefs() - .edit { putInt(TIMES_STORAGE_MIGRATION_POSTPONED_KEY, value) } + set(value) = + baseContext.sharedPrefs() + .edit { putInt(TIMES_STORAGE_MIGRATION_POSTPONED_KEY, value) } /** Whether the user has disabled the dialog from [showDialogThatOffersToMigrateStorage] */ private val disabledScopedStorageReminder: Boolean @@ -2295,28 +2410,29 @@ open class DeckPicker : putInt(TIMES_STORAGE_MIGRATION_POSTPONED_KEY, -1) remove(MIGRATION_WAS_LAST_POSTPONED_AT_SECONDS) } - } + }, ) } var userCheckedDoNotShowAgain = false - var dialog = AlertDialog.Builder(this) - .setTitle(R.string.scoped_storage_title) - .setMessage(message) - .setPositiveButton( - getString(R.string.scoped_storage_migrate) - ) { _, _ -> - performMediaSyncBeforeStorageMigration() - } - .setNegativeButton( - getString(R.string.scoped_storage_postpone) - ) { _, _ -> - if (userCheckedDoNotShowAgain) { - onPostponePermanently() - } else { - onPostponeOnce() + var dialog = + AlertDialog.Builder(this) + .setTitle(R.string.scoped_storage_title) + .setMessage(message) + .setPositiveButton( + getString(R.string.scoped_storage_migrate), + ) { _, _ -> + performMediaSyncBeforeStorageMigration() + } + .setNegativeButton( + getString(R.string.scoped_storage_postpone), + ) { _, _ -> + if (userCheckedDoNotShowAgain) { + onPostponePermanently() + } else { + onPostponeOnce() + } } - } // allow the user to dismiss the automatic dialog after it's been seen twice if (shownAutomatically && timesStorageMigrationPostponed > 1) { dialog.checkBoxPrompt(R.string.button_do_not_show_again) { checked -> @@ -2329,8 +2445,9 @@ open class DeckPicker : 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() + val message = + getString(R.string.migration__resume_after_failed_dialog__message, errorText, helpUrl) + .parseAsHtml() AlertDialog.Builder(this) .setTitle(R.string.scoped_storage_title) @@ -2412,17 +2529,16 @@ open class DeckPicker : * * @param did The id of a deck with no pending cards to review */ - private suspend fun queryCompletedDeckCustomStudyAction( - did: DeckId - ): CompletedDeckStatus = withCol { - when { - sched.hasCardsTodayAfterStudyAheadLimit() -> CompletedDeckStatus.LEARN_AHEAD_LIMIT_REACHED - sched.newDue() || sched.revDue() -> CompletedDeckStatus.LEARN_AHEAD_LIMIT_REACHED - decks.isDyn(did) -> CompletedDeckStatus.DYNAMIC_DECK_NO_LIMITS_REACHED - mDeckListAdapter.getNodeByDid(did).children.isEmpty() && isEmptyDeck(did) -> CompletedDeckStatus.EMPTY_REGULAR_DECK - else -> CompletedDeckStatus.REGULAR_DECK_NO_MORE_CARDS_TODAY + private suspend fun queryCompletedDeckCustomStudyAction(did: DeckId): CompletedDeckStatus = + withCol { + when { + sched.hasCardsTodayAfterStudyAheadLimit() -> CompletedDeckStatus.LEARN_AHEAD_LIMIT_REACHED + sched.newDue() || sched.revDue() -> CompletedDeckStatus.LEARN_AHEAD_LIMIT_REACHED + decks.isDyn(did) -> CompletedDeckStatus.DYNAMIC_DECK_NO_LIMITS_REACHED + mDeckListAdapter.getNodeByDid(did).children.isEmpty() && isEmptyDeck(did) -> CompletedDeckStatus.EMPTY_REGULAR_DECK + else -> CompletedDeckStatus.REGULAR_DECK_NO_MORE_CARDS_TODAY + } } - } /** Status for a deck with no current cards to review */ enum class CompletedDeckStatus { @@ -2439,7 +2555,7 @@ open class DeckPicker : EMPTY_REGULAR_DECK, /** The user has completed their studying for today, and there are future reviews */ - REGULAR_DECK_NO_MORE_CARDS_TODAY + REGULAR_DECK_NO_MORE_CARDS_TODAY, } override fun getApkgFileImportResultLauncher(): ActivityResultLauncher { @@ -2463,19 +2579,19 @@ data class OptionsMenuState( val undoLabel: String?, val syncIcon: SyncIconState, val shouldShowStartMigrationButton: Boolean, - val mediaMigrationState: MediaMigrationState + val mediaMigrationState: MediaMigrationState, ) enum class SyncIconState { Normal, PendingChanges, FullSync, - NotLoggedIn + NotLoggedIn, } class CollectionLoadingErrorDialog : DialogHandlerMessage( WhichDialogHandler.MSG_SHOW_COLLECTION_LOADING_ERROR_DIALOG, - "CollectionLoadErrorDialog" + "CollectionLoadErrorDialog", ) { override fun handleAsyncMessage(deckPicker: DeckPicker) { // Collection could not be opened @@ -2487,35 +2603,38 @@ class CollectionLoadingErrorDialog : DialogHandlerMessage( class ForceFullSyncDialog(val message: String?) : DialogHandlerMessage( which = WhichDialogHandler.MSG_SHOW_FORCE_FULL_SYNC_DIALOG, - analyticName = "ForceFullSyncDialog" + analyticName = "ForceFullSyncDialog", ) { override fun handleAsyncMessage(deckPicker: DeckPicker) { // Confirmation dialog for forcing full sync val dialog = ConfirmationDialog() - val confirm = Runnable { - // Bypass the check once the user confirms - CollectionHelper.instance.getColUnsafe(AnkiDroidApp.instance)!!.modSchemaNoCheck() - } + val confirm = + Runnable { + // Bypass the check once the user confirms + CollectionHelper.instance.getColUnsafe(AnkiDroidApp.instance)!!.modSchemaNoCheck() + } dialog.setConfirm(confirm) dialog.setArgs(message) deckPicker.showDialogFragment(dialog) } - override fun toMessage(): Message = Message.obtain().apply { - what = this@ForceFullSyncDialog.what - data = bundleOf("message" to message) - } + override fun toMessage(): Message = + Message.obtain().apply { + what = this@ForceFullSyncDialog.what + data = bundleOf("message" to message) + } companion object { - fun fromMessage(message: Message): DialogHandlerMessage = - ForceFullSyncDialog(message.data.getString("message")) + fun fromMessage(message: Message): DialogHandlerMessage = ForceFullSyncDialog(message.data.getString("message")) } } // This is used to re-show the dialog immediately on activity recreation -private suspend fun Activity.withImmediatelyShownProgress(@StringRes messageId: Int, block: suspend () -> T) = - withProgressDialog(context = this, onCancel = null, delayMillis = 0L) { dialog -> - @Suppress("DEPRECATION") // ProgressDialog - dialog.setMessage(getString(messageId)) - block() - } +private suspend fun Activity.withImmediatelyShownProgress( + @StringRes messageId: Int, + block: suspend () -> T, +) = withProgressDialog(context = this, onCancel = null, delayMillis = 0L) { dialog -> + @Suppress("DEPRECATION") // ProgressDialog + dialog.setMessage(getString(messageId)) + block() +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt index 270637bcd09a..279cc4eef3ef 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt @@ -32,7 +32,7 @@ import timber.log.Timber class DeckPickerFloatingActionMenu( private val context: Context, view: View, - private val deckPicker: DeckPicker + private val deckPicker: DeckPicker, ) { private val mFabMain: FloatingActionButton = view.findViewById(R.id.fab_main) private val mAddSharedLayout: LinearLayout = view.findViewById(R.id.add_shared_layout) @@ -167,35 +167,43 @@ class DeckPickerFloatingActionMenu( mAddSharedLayout.animate().translationY(400f).duration = 100 addNoteLabel.animate().translationX(180f).duration = 70 mAddDeckLayout.animate().translationY(300f).setDuration(50) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - mAddSharedLayout.visibility = View.GONE - mAddDeckLayout.visibility = View.GONE - mAddFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + mAddSharedLayout.visibility = View.GONE + mAddDeckLayout.visibility = View.GONE + mAddFilteredDeckLayout.visibility = View.GONE + addNoteLabel.visibility = View.GONE + } } - } - override fun onAnimationCancel(animator: Animator) {} - override fun onAnimationRepeat(animator: Animator) {} - }) + override fun onAnimationCancel(animator: Animator) {} + + override fun onAnimationRepeat(animator: Animator) {} + }, + ) mAddFilteredDeckLayout.animate().translationY(400f).setDuration(100) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - mAddSharedLayout.visibility = View.GONE - mAddDeckLayout.visibility = View.GONE - mAddFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + mAddSharedLayout.visibility = View.GONE + mAddDeckLayout.visibility = View.GONE + mAddFilteredDeckLayout.visibility = View.GONE + addNoteLabel.visibility = View.GONE + } } - } - override fun onAnimationCancel(animator: Animator) {} - override fun onAnimationRepeat(animator: Animator) {} - }) + override fun onAnimationCancel(animator: Animator) {} + + override fun onAnimationRepeat(animator: Animator) {} + }, + ) } else { // Close without animation mAddSharedLayout.visibility = View.GONE @@ -230,35 +238,43 @@ class DeckPickerFloatingActionMenu( addNoteLabel.animate().translationX(180f).duration = 70 mAddSharedLayout.animate().translationY(600f).duration = 100 mAddDeckLayout.animate().translationY(400f).setDuration(50) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - mAddSharedLayout.visibility = View.GONE - mAddDeckLayout.visibility = View.GONE - mAddFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + mAddSharedLayout.visibility = View.GONE + mAddDeckLayout.visibility = View.GONE + mAddFilteredDeckLayout.visibility = View.GONE + addNoteLabel.visibility = View.GONE + } } - } - override fun onAnimationCancel(animator: Animator) {} - override fun onAnimationRepeat(animator: Animator) {} - }) + override fun onAnimationCancel(animator: Animator) {} + + override fun onAnimationRepeat(animator: Animator) {} + }, + ) mAddFilteredDeckLayout.animate().translationY(600f).setDuration(100) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - mAddSharedLayout.visibility = View.GONE - mAddDeckLayout.visibility = View.GONE - mAddFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + mAddSharedLayout.visibility = View.GONE + mAddDeckLayout.visibility = View.GONE + mAddFilteredDeckLayout.visibility = View.GONE + addNoteLabel.visibility = View.GONE + } } - } - override fun onAnimationCancel(animator: Animator) {} - override fun onAnimationRepeat(animator: Animator) {} - }) + override fun onAnimationCancel(animator: Animator) {} + + override fun onAnimationRepeat(animator: Animator) {} + }, + ) } else { // Close without animation mAddSharedLayout.visibility = View.GONE @@ -293,21 +309,24 @@ class DeckPickerFloatingActionMenu( * WINDOW_ANIMATION_SCALE - controls pop-up window opening and closing animation speed */ private fun areSystemAnimationsEnabled(): Boolean { - val animDuration: Float = Settings.Global.getFloat( - context.contentResolver, - Settings.Global.ANIMATOR_DURATION_SCALE, - 1f - ) - val animTransition: Float = Settings.Global.getFloat( - context.contentResolver, - Settings.Global.TRANSITION_ANIMATION_SCALE, - 1f - ) - val animWindow: Float = Settings.Global.getFloat( - context.contentResolver, - Settings.Global.WINDOW_ANIMATION_SCALE, - 1f - ) + val animDuration: Float = + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) + val animTransition: Float = + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.TRANSITION_ANIMATION_SCALE, + 1f, + ) + val animWindow: Float = + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.WINDOW_ANIMATION_SCALE, + 1f, + ) return animDuration != 0f && animTransition != 0f && animWindow != 0f } @@ -319,61 +338,68 @@ class DeckPickerFloatingActionMenu( val addDeckLabel: TextView = view.findViewById(R.id.add_deck_label) val addFilteredDeckLabel: TextView = view.findViewById(R.id.add_filtered_deck_label) val addNote: TextView = view.findViewById(R.id.add_note_label) - mFabMain.setOnTouchListener(object : DoubleTapListener(context) { - override fun onDoubleTap(e: MotionEvent?) { - addNote() - } - - override fun onUnconfirmedSingleTap(e: MotionEvent?) { - // we use an unconfirmed tap as we don't want any visual delay in tapping the + - // and opening the menu. - if (!isFABOpen) { - showFloatingActionMenu() - } else { + mFabMain.setOnTouchListener( + object : DoubleTapListener(context) { + override fun onDoubleTap(e: MotionEvent?) { addNote() } - } - }) + + override fun onUnconfirmedSingleTap(e: MotionEvent?) { + // we use an unconfirmed tap as we don't want any visual delay in tapping the + + // and opening the menu. + if (!isFABOpen) { + showFloatingActionMenu() + } else { + addNote() + } + } + }, + ) mFabBGLayout.setOnClickListener { closeFloatingActionMenu(applyRiseAndShrinkAnimation = true) } - val addDeckListener = View.OnClickListener { - if (isFABOpen) { - closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) - val createDeckDialog = CreateDeckDialog( - context, - R.string.new_deck, - CreateDeckDialog.DeckDialogType.DECK, - null - ) - createDeckDialog.setOnNewDeckCreated { deckPicker.updateDeckList() } - createDeckDialog.showDialog() + val addDeckListener = + View.OnClickListener { + if (isFABOpen) { + closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) + val createDeckDialog = + CreateDeckDialog( + context, + R.string.new_deck, + CreateDeckDialog.DeckDialogType.DECK, + null, + ) + createDeckDialog.setOnNewDeckCreated { deckPicker.updateDeckList() } + createDeckDialog.showDialog() + } } - } addDeckButton.setOnClickListener(addDeckListener) addDeckLabel.setOnClickListener(addDeckListener) - val addFilteredDeckListener = View.OnClickListener { - if (isFABOpen) { - closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) - deckPicker.createFilteredDialog() + val addFilteredDeckListener = + View.OnClickListener { + if (isFABOpen) { + closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) + deckPicker.createFilteredDialog() + } } - } addFilteredDeckButton.setOnClickListener(addFilteredDeckListener) addFilteredDeckLabel.setOnClickListener(addFilteredDeckListener) - val addSharedListener = View.OnClickListener { - if (isFABOpen) { - closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) - Timber.d("configureFloatingActionsMenu::addSharedButton::onClickListener - Adding Shared Deck") - deckPicker.openAnkiWebSharedDecks() + val addSharedListener = + View.OnClickListener { + if (isFABOpen) { + closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) + Timber.d("configureFloatingActionsMenu::addSharedButton::onClickListener - Adding Shared Deck") + deckPicker.openAnkiWebSharedDecks() + } } - } addSharedButton.setOnClickListener(addSharedListener) addSharedLabel.setOnClickListener(addSharedListener) - val addNoteLabelListener = View.OnClickListener { - if (isFABOpen) { - closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) - Timber.d("configureFloatingActionsMenu::addNoteLabel::onClickListener - Adding Note") - addNote() + val addNoteLabelListener = + View.OnClickListener { + if (isFABOpen) { + closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) + Timber.d("configureFloatingActionsMenu::addNoteLabel::onClickListener - Adding Note") + addNote() + } } - } addNote.setOnClickListener(addNoteLabelListener) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt index 8659652604e1..599b006f7165 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt @@ -53,7 +53,7 @@ class DeckSpinnerSelection( private val spinner: Spinner, private val showAllDecks: Boolean, private val alwaysShowDefault: Boolean, - private val showFilteredDecks: Boolean + private val showFilteredDecks: Boolean, ) { /** * All of the decks shown to the user. @@ -89,21 +89,26 @@ class DeckSpinnerSelection( deckNames.add(currentName) mAllDeckIds.add(d.id) } - val noteDeckAdapter: ArrayAdapter = object : ArrayAdapter(context, R.layout.multiline_spinner_item, deckNames as List) { - override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { - // Cast the drop down items (popup items) as text view - val tv = super.getDropDownView(position, convertView, parent) as TextView - - // If this item is selected - if (position == spinner.selectedItemPosition) { - tv.setBackgroundColor(context.getColor(R.color.note_editor_selected_item_background)) - tv.setTextColor(context.getColor(R.color.note_editor_selected_item_text)) + val noteDeckAdapter: ArrayAdapter = + object : ArrayAdapter(context, R.layout.multiline_spinner_item, deckNames as List) { + override fun getDropDownView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { + // Cast the drop down items (popup items) as text view + val tv = super.getDropDownView(position, convertView, parent) as TextView + + // If this item is selected + if (position == spinner.selectedItemPosition) { + tv.setBackgroundColor(context.getColor(R.color.note_editor_selected_item_background)) + tv.setTextColor(context.getColor(R.color.note_editor_selected_item_text)) + } + + // Return the modified view + return tv } - - // Return the modified view - return tv } - } spinner.adapter = noteDeckAdapter setSpinnerListener() } @@ -160,7 +165,10 @@ class DeckSpinnerSelection( * the current deck id of Collection. * @return True if selection succeeded. */ - fun selectDeckById(deckId: DeckId, setAsCurrentDeck: Boolean): Boolean { + fun selectDeckById( + deckId: DeckId, + setAsCurrentDeck: Boolean, + ): Boolean { return if (deckId == ALL_DECKS_ID) { selectAllDecks() } else { @@ -174,7 +182,10 @@ class DeckSpinnerSelection( * @param setAsCurrentDeck whether this deck should be selected in the collection (if it exists) * @return whether it was found */ - private fun selectDeck(deckId: DeckId, setAsCurrentDeck: Boolean): Boolean { + private fun selectDeck( + deckId: DeckId, + setAsCurrentDeck: Boolean, + ): Boolean { for (dropDownDeckIdx in mAllDeckIds.indices) { if (mAllDeckIds[dropDownDeckIdx] == deckId) { val position = if (showAllDecks) dropDownDeckIdx + 1 else dropDownDeckIdx @@ -194,7 +205,10 @@ class DeckSpinnerSelection( */ fun selectAllDecks(): Boolean { if (!showAllDecks) { - CrashReportService.sendExceptionReport("selectAllDecks was called while `showAllDecks is false`", "DeckSpinnerSelection:selectAllDecks") + CrashReportService.sendExceptionReport( + "selectAllDecks was called while `showAllDecks is false`", + "DeckSpinnerSelection:selectAllDecks", + ) return false } spinner.setSelection(0) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt index a43e3f01ad70..f2c545a70312 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt @@ -60,8 +60,8 @@ class DrawingActivity : AnkiActivity() { whiteboardEditItem, ContextCompat.getColorStateList( this, - R.color.white - ) + R.color.white, + ), ) return super.onCreateOptionsMenu(menu) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt index 674ffdd256d5..502480bc9479 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt @@ -90,16 +90,17 @@ class FieldEditLine : FrameLayout { } private fun toggleExpansionState() { - mExpansionState = when (mExpansionState) { - ExpansionState.EXPANDED -> { - collapseView(editText, mEnableAnimation) - ExpansionState.COLLAPSED - } - ExpansionState.COLLAPSED -> { - expandView(editText, mEnableAnimation) - ExpansionState.EXPANDED + mExpansionState = + when (mExpansionState) { + ExpansionState.EXPANDED -> { + collapseView(editText, mEnableAnimation) + ExpansionState.COLLAPSED + } + ExpansionState.COLLAPSED -> { + expandView(editText, mEnableAnimation) + ExpansionState.EXPANDED + } } - } setExpanderBackgroundImage() } @@ -110,7 +111,9 @@ class FieldEditLine : FrameLayout { } } - private fun getBackgroundImage(@DrawableRes idRes: Int): Drawable? { + private fun getBackgroundImage( + @DrawableRes idRes: Int, + ): Drawable? { return VectorDrawableCompat.create(this.resources, idRes, context.theme) } @@ -131,7 +134,10 @@ class FieldEditLine : FrameLayout { } } - fun setContent(content: String?, replaceNewline: Boolean) { + fun setContent( + content: String?, + replaceNewline: Boolean, + ) { editText.setContent(content, replaceNewline) } @@ -221,7 +227,10 @@ class FieldEditLine : FrameLayout { constructor(superState: Parcelable?) : super(superState) - override fun writeToParcel(out: Parcel, flags: Int) { + override fun writeToParcel( + out: Parcel, + flags: Int, + ) { super.writeToParcel(out, flags) out.writeSparseArray(childrenStates) out.writeInt(editTextId) @@ -237,33 +246,39 @@ class FieldEditLine : FrameLayout { toggleStickyId = source.readInt() mediaButtonId = source.readInt() expandButtonId = source.readInt() - expansionState = ParcelCompat.readSerializable( - source, - ExpansionState::class.java.classLoader, - ExpansionState::class.java - ) + expansionState = + ParcelCompat.readSerializable( + source, + ExpansionState::class.java.classLoader, + ExpansionState::class.java, + ) } companion object { @JvmField // required field that makes Parcelables from a Parcel @Suppress("unused") - val CREATOR: Parcelable.Creator = object : ClassLoaderCreator { - override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState { - return SavedState(source, loader) - } + val CREATOR: Parcelable.Creator = + object : ClassLoaderCreator { + override fun createFromParcel( + source: Parcel, + loader: ClassLoader, + ): SavedState { + return SavedState(source, loader) + } - override fun createFromParcel(source: Parcel): SavedState { - throw IllegalStateException() - } + override fun createFromParcel(source: Parcel): SavedState { + throw IllegalStateException() + } - override fun newArray(size: Int): Array { - return arrayOfNulls(size) + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } - } } } enum class ExpansionState { - EXPANDED, COLLAPSED + EXPANDED, + COLLAPSED, } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt index 713024671d63..8ceb2cc53d7a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt @@ -108,7 +108,10 @@ class FieldEditText : FixedEditText, NoteService.NoteField { this, IMAGE_MIME_TYPES, object : OnReceiveContentListener { - override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? { + override fun onReceiveContent( + view: View, + payload: ContentInfoCompat, + ): ContentInfoCompat? { val pair = payload.partition { item -> item.uri != null } val uriContent = pair.first val remaining = pair.second @@ -137,13 +140,16 @@ class FieldEditText : FixedEditText, NoteService.NoteField { return remaining } - } + }, ) return InputConnectionCompat.createWrapper(this, inputConnection, editorInfo) } - override fun onSelectionChanged(selStart: Int, selEnd: Int) { + override fun onSelectionChanged( + selStart: Int, + selEnd: Int, + ) { if (mSelectionChangeListener != null) { try { mSelectionChangeListener!!.onSelectionChanged(selStart, selEnd) @@ -174,14 +180,18 @@ class FieldEditText : FixedEditText, NoteService.NoteField { background = mOrigBackground } - fun setContent(content: String?, replaceNewLine: Boolean) { - val text = if (content == null) { - "" - } else if (replaceNewLine) { - content.replace("".toRegex(), NEW_LINE) - } else { - content - } + fun setContent( + content: String?, + replaceNewLine: Boolean, + ) { + val text = + if (content == null) { + "" + } else if (replaceNewLine) { + content.replace("".toRegex(), NEW_LINE) + } else { + content + } setText(text) } @@ -207,7 +217,7 @@ class FieldEditText : FixedEditText, NoteService.NoteField { val start = min(selectionStart, selectionEnd) val end = max(selectionStart, selectionEnd) setText( - text!!.substring(0, start) + pasted + text!!.substring(end) + text!!.substring(0, start) + pasted + text!!.substring(end), ) setSelection(start + pasted.length) return true @@ -234,11 +244,12 @@ class FieldEditText : FixedEditText, NoteService.NoteField { fun setCapitalize(value: Boolean) { val inputType = this.inputType - this.inputType = if (value) { - inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - } else { - inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv() - } + this.inputType = + if (value) { + inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + } else { + inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv() + } } val isCapitalized: Boolean @@ -248,11 +259,17 @@ class FieldEditText : FixedEditText, NoteService.NoteField { internal class SavedState(val state: Parcelable?, val ord: Int) : BaseSavedState(state) interface TextSelectionListener { - fun onSelectionChanged(selStart: Int, selEnd: Int) + fun onSelectionChanged( + selStart: Int, + selEnd: Int, + ) } fun interface ImagePasteListener { - fun onImagePaste(editText: EditText, uri: Uri?): Boolean + fun onImagePaste( + editText: EditText, + uri: Uri?, + ): Boolean } companion object { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FilteredDeckOptions.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FilteredDeckOptions.kt index 01fde1c7705e..ebe2e904d0f0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FilteredDeckOptions.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FilteredDeckOptions.kt @@ -40,19 +40,21 @@ class FilteredDeckOptions : private var mAllowCommit = true // TODO: not anymore used in libanki? - private val mDynExamples = arrayOf( - null, - "{'search'=\"is:new\", 'resched'=False, 'steps'=\"1\", 'order'=5}", - "{'search'=\"added:1\", 'resched'=False, 'steps'=\"1\", 'order'=5}", - "{'search'=\"rated:1:1\", 'order'=4}", - "{'search'=\"prop:due<=2\", 'order'=6}", - "{'search'=\"is:due tag:TAG\", 'order'=6}", - "{'search'=\"is:due\", 'order'=3}", - "{'search'=\"\", 'steps'=\"1 10 20\", 'order'=0}" - ) + private val mDynExamples = + arrayOf( + null, + "{'search'=\"is:new\", 'resched'=False, 'steps'=\"1\", 'order'=5}", + "{'search'=\"added:1\", 'resched'=False, 'steps'=\"1\", 'order'=5}", + "{'search'=\"rated:1:1\", 'order'=4}", + "{'search'=\"prop:due<=2\", 'order'=6}", + "{'search'=\"is:due tag:TAG\", 'order'=6}", + "{'search'=\"is:due\", 'order'=3}", + "{'search'=\"\", 'steps'=\"1 10 20\", 'order'=0}", + ) inner class DeckPreferenceHack : AppCompatPreferenceActivity.AbstractPreferenceHack() { var secondFilter = false + override fun cacheValues() { Timber.d("cacheValues()") val ar = deck.getJSONArray("terms").getJSONArray(0) @@ -212,12 +214,13 @@ class FilteredDeckOptions : // Set the activity title to include the name of the deck var title = resources.getString(R.string.deckpreferences_title) if (title.contains("XXX")) { - title = try { - title.replace("XXX", deck.getString("name")) - } catch (e: JSONException) { - Timber.w(e) - title.replace("XXX", "???") - } + title = + try { + title.replace("XXX", deck.getString("name")) + } catch (e: JSONException) { + Timber.w(e) + title.replace("XXX", "???") + } } this.title = title @@ -271,16 +274,17 @@ class FilteredDeckOptions : val keys: Set = pref.mValues.keys for (key in keys) { val pref = findPreference(key) - val value: String? = if (pref == null) { - continue - } else if (pref is CheckBoxPreference) { - continue - } else if (pref is ListPreference) { - val entry = pref.entry - entry?.toString() ?: "" - } else { - this.pref.getString(key, "") - } + val value: String? = + if (pref == null) { + continue + } else if (pref is CheckBoxPreference) { + continue + } else if (pref is ListPreference) { + val entry = pref.entry + entry?.toString() ?: "" + } else { + this.pref.getString(key, "") + } // update value for EditTexts if (pref is EditTextPreference) { pref.text = value @@ -322,24 +326,25 @@ class FilteredDeckOptions : secondFilter.isEnabled = true secondFilterSign.isChecked = true } - secondFilterSign.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> - if (newValue !is Boolean) { - return@OnPreferenceChangeListener true - } - if (!newValue) { - deck.getJSONArray("terms").remove(1) - secondFilter.isEnabled = false - } else { - secondFilter.isEnabled = true - /**Link to the defaults used in AnkiDesktop - * - */ - val narr = JSONArray(listOf("", 20, 5)) - deck.getJSONArray("terms").put(1, narr) - val newOrderPrefSecond = findPreference("order_2") as ListPreference - newOrderPrefSecond.value = "5" + secondFilterSign.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> + if (newValue !is Boolean) { + return@OnPreferenceChangeListener true + } + if (!newValue) { + deck.getJSONArray("terms").remove(1) + secondFilter.isEnabled = false + } else { + secondFilter.isEnabled = true + /**Link to the defaults used in AnkiDesktop + * + */ + val narr = JSONArray(listOf("", 20, 5)) + deck.getJSONArray("terms").put(1, narr) + val newOrderPrefSecond = findPreference("order_2") as ListPreference + newOrderPrefSecond.value = "5" + } + true } - true - } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt index 5f6a5c16a115..cf611101bdbd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt @@ -17,7 +17,10 @@ package com.ichi2.anki import androidx.annotation.DrawableRes -enum class Flag(val code: Int, @DrawableRes val drawableRes: Int) { +enum class Flag( + val code: Int, + @DrawableRes val drawableRes: Int, +) { NONE(0, R.drawable.ic_flag_transparent), RED(1, R.drawable.ic_flag_red), ORANGE(2, R.drawable.ic_flag_orange), @@ -25,7 +28,8 @@ enum class Flag(val code: Int, @DrawableRes val drawableRes: Int) { BLUE(4, R.drawable.ic_flag_blue), PINK(5, R.drawable.ic_flag_pink), TURQUOISE(6, R.drawable.ic_flag_turquoise), - PURPLE(7, R.drawable.ic_flag_purple); + PURPLE(7, R.drawable.ic_flag_purple), + ; companion object { fun fromCode(code: Int): Flag { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt index 1ad326dcff5c..078b3cf1fa65 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt @@ -18,9 +18,8 @@ package com.ichi2.anki class FlagToDisplay( private val actualFlag: Int, private val isOnAppBar: Boolean, - private val isFullscreen: Boolean + private val isFullscreen: Boolean, ) { - fun get(): Int { return when { !isOnAppBar -> actualFlag diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt index 0bed557eb0b4..c7c10b30f940 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt @@ -56,18 +56,22 @@ fun DeckPicker.onSelectedCsvForImport(data: Intent) { startActivity(CsvImporter.getIntent(this, path)) } -fun DeckPicker.showImportDialog(id: Int, importPath: String) { +fun DeckPicker.showImportDialog( + id: Int, + importPath: String, +) { Timber.d("showImportDialog() delegating to ImportDialog") val newFragment: AsyncDialogFragment = ImportDialog.newInstance(id, importPath) showAsyncDialogFragment(newFragment) } + fun DeckPicker.showImportDialog() { showImportDialog( ImportOptions( importApkg = true, importColpkg = true, - importTextFile = true - ) + importTextFile = true, + ), ) } @@ -75,7 +79,7 @@ fun DeckPicker.showImportDialog(options: ImportOptions) { if (ScopedStorageService.mediaMigrationIsInProgress(this)) { showSnackbar( R.string.functionality_disabled_during_storage_migration, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ) return } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt index 36e7a082ac7f..245bad50ee44 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt @@ -64,35 +64,42 @@ class Info : AnkiActivity() { setContentView(R.layout.info) val mainView = findViewById(android.R.id.content) enableToolbar(mainView) - findViewById(R.id.info_donate).setOnClickListener { openUrl(Uri.parse(getString(R.string.link_opencollective_donate))) } + findViewById( + R.id.info_donate, + ).setOnClickListener { openUrl(Uri.parse(getString(R.string.link_opencollective_donate))) } title = "$appName v$pkgVersionName" mWebView = findViewById(R.id.info) - mWebView.webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView, progress: Int) { - // Hide the progress indicator when the page has finished loaded - if (progress == 100) { - mainView.findViewById(R.id.progress_bar).visibility = View.GONE + mWebView.webChromeClient = + object : WebChromeClient() { + override fun onProgressChanged( + view: WebView, + progress: Int, + ) { + // Hide the progress indicator when the page has finished loaded + if (progress == 100) { + mainView.findViewById(R.id.progress_bar).visibility = View.GONE + } } } - } findViewById(R.id.left_button).run { if (canOpenMarketUri()) { setText(R.string.info_rate) setOnClickListener { tryOpenIntent( this@Info, - AnkiDroidApp.getMarketIntent(this@Info) + AnkiDroidApp.getMarketIntent(this@Info), ) } } else { visibility = View.GONE } } - val onBackPressedCallback = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - if (mWebView.canGoBack()) mWebView.goBack() + val onBackPressedCallback = + object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + if (mWebView.canGoBack()) mWebView.goBack() + } } - } // Apply Theme colors val typedArray = theme.obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground, android.R.attr.textColor)) val backgroundColor = typedArray.getColor(0, -1) @@ -114,52 +121,56 @@ class Info : AnkiActivity() { val background = backgroundColor.toRGBHex() mWebView.loadUrl("/android_asset/changelog.html") mWebView.settings.javaScriptEnabled = true - mWebView.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView, url: String) { + mWebView.webViewClient = + object : WebViewClient() { + override fun onPageFinished( + view: WebView, + url: String, + ) { /* The order of below javascript code must not change (this order works both in debug and release mode) - * or else it will break in any one mode. - */ - mWebView.loadUrl( - "javascript:document.body.style.setProperty(\"color\", \"" + textColor + "\");" + - "x=document.getElementsByTagName(\"a\"); for(i=0;i finish() } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt index 1890bddb311d..33d8dc63c6fa 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt @@ -70,7 +70,10 @@ object InitialActivity { /** @return Whether any preferences were upgraded */ - fun upgradePreferences(context: Context, previousVersionCode: Long): Boolean { + fun upgradePreferences( + context: Context, + previousVersionCode: Long, + ): Boolean { return PreferenceUpgradeService.upgradePreferences(context, previousVersionCode) } @@ -103,8 +106,7 @@ object InitialActivity { * false if the app was launched for the second time after a successful initialisation * false if the app was launched after an update */ - fun wasFreshInstall(preferences: SharedPreferences) = - "" == preferences.getString("lastVersion", "") + fun wasFreshInstall(preferences: SharedPreferences) = "" == preferences.getString("lastVersion", "") /** Sets the preference stating that the latest version has been applied */ fun setUpgradedToLatestVersion(preferences: SharedPreferences) { @@ -122,8 +124,13 @@ object InitialActivity { } enum class StartupFailure { - SD_CARD_NOT_MOUNTED, DIRECTORY_NOT_ACCESSIBLE, FUTURE_ANKIDROID_VERSION, - DB_ERROR, DATABASE_LOCKED, WEBVIEW_FAILED, DISK_FULL + SD_CARD_NOT_MOUNTED, + DIRECTORY_NOT_ACCESSIBLE, + FUTURE_ANKIDROID_VERSION, + DB_ERROR, + DATABASE_LOCKED, + WEBVIEW_FAILED, + DISK_FULL, } } @@ -160,10 +167,10 @@ enum class PermissionSet(val permissions: List, val permissionsFragment: @RequiresApi(Build.VERSION_CODES.TIRAMISU) TIRAMISU_EXTERNAL_MANAGER( permissions = listOf(Permissions.MANAGE_EXTERNAL_STORAGE), - permissionsFragment = TiramisuPermissionsFragment::class.java + permissionsFragment = TiramisuPermissionsFragment::class.java, ), - APP_PRIVATE(emptyList(), null); + APP_PRIVATE(emptyList(), null), } /** @@ -174,7 +181,7 @@ enum class PermissionSet(val permissions: List, val permissionsFragment: */ internal fun selectAnkiDroidFolder( canManageExternalStorage: Boolean, - currentFolderIsAccessibleAndLegacy: Boolean + currentFolderIsAccessibleAndLegacy: Boolean, ): AnkiDroidFolder { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q || currentFolderIsAccessibleAndLegacy) { // match AnkiDroid behaviour before scoped storage - force the use of ~/AnkiDroid, @@ -202,6 +209,6 @@ fun selectAnkiDroidFolder(context: Context): AnkiDroidFolder { return selectAnkiDroidFolder( canManageExternalStorage = Permissions.canManageExternalStorage(context), - currentFolderIsAccessibleAndLegacy = currentFolderIsAccessibleAndLegacy + currentFolderIsAccessibleAndLegacy = currentFolderIsAccessibleAndLegacy, ) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt index cfa6320a8e03..7c16efc0a1d2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt @@ -104,7 +104,11 @@ class IntentHandler : Activity() { * */ @NeedsTest("clicking a file in 'Files' to import") - private fun performActionIfStorageAccessible(runnable: Runnable, reloadIntent: Intent, action: String?) { + private fun performActionIfStorageAccessible( + runnable: Runnable, + reloadIntent: Intent, + action: String?, + ) { if (!ScopedStorageService.isLegacyStorage(this) || hasStorageAccessPermission(this) || Permissions.isExternalStorageManagerCompat()) { Timber.i("User has storage permissions. Running intent: %s", action) runnable.run() @@ -124,7 +128,10 @@ class IntentHandler : Activity() { finish() } - private fun handleSyncIntent(reloadIntent: Intent, action: String?) { + private fun handleSyncIntent( + reloadIntent: Intent, + action: String?, + ) { Timber.i("Handling Sync Intent") sendDoSyncMsg() reloadIntent.action = action @@ -133,7 +140,11 @@ class IntentHandler : Activity() { finish() } - private fun handleFileImport(intent: Intent, reloadIntent: Intent, action: String?) { + private fun handleFileImport( + intent: Intent, + reloadIntent: Intent, + action: String?, + ) { Timber.i("Handling file import") val importResult = handleFileImport(this, intent) // attempt to delete the downloaded deck if it is a shared deck download import @@ -156,13 +167,14 @@ class IntentHandler : Activity() { if (importResult.isSuccess) { try { val file = File(intent.data!!.path!!) - val fileUri = applicationContext?.let { - FileProvider.getUriForFile( - it, - it.applicationContext?.packageName + ".apkgfileprovider", - File(it.getExternalFilesDir(FileUtil.getDownloadDirectory()), file.name) - ) - } + val fileUri = + applicationContext?.let { + FileProvider.getUriForFile( + it, + it.applicationContext?.packageName + ".apkgfileprovider", + File(it.getExternalFilesDir(FileUtil.getDownloadDirectory()), file.name), + ) + } // TODO move the file deletion on a background thread contentResolver.delete(fileUri!!, null, null) Timber.i("onCreate() import successful and downloaded file deleted") @@ -195,7 +207,11 @@ class IntentHandler : Activity() { // COULD_BE_BETTER: Also extract the parameters into here to reduce coupling @VisibleForTesting enum class LaunchType { - DEFAULT_START_APP_IF_NEW, FILE_IMPORT, SYNC, REVIEW, COPY_DEBUG_INFO + DEFAULT_START_APP_IF_NEW, + FILE_IMPORT, + SYNC, + REVIEW, + COPY_DEBUG_INFO, } companion object { @@ -233,17 +249,19 @@ class IntentHandler : Activity() { storeMessage(DoSync().toMessage()) } - fun copyStringToClipboardIntent(context: Context, textToCopy: String) = - Intent(context, IntentHandler::class.java).also { - it.action = CLIPBOARD_INTENT - // max length for an intent is 500KB. - // 25000 * 2 (bytes per char) = 50,000 bytes <<< 500KB - it.putExtra(CLIPBOARD_INTENT_EXTRA_DATA, textToCopy.trimToLength(25000)) - } + fun copyStringToClipboardIntent( + context: Context, + textToCopy: String, + ) = Intent(context, IntentHandler::class.java).also { + it.action = CLIPBOARD_INTENT + // max length for an intent is 500KB. + // 25000 * 2 (bytes per char) = 50,000 bytes <<< 500KB + it.putExtra(CLIPBOARD_INTENT_EXTRA_DATA, textToCopy.trimToLength(25000)) + } class DoSync : DialogHandlerMessage( which = WhichDialogHandler.MSG_DO_SYNC, - analyticName = "DoSyncDialog" + analyticName = "DoSyncDialog", ) { override fun handleAsyncMessage(deckPicker: DeckPicker) { val preferences = deckPicker.sharedPrefs() @@ -260,17 +278,18 @@ class IntentHandler : Activity() { max((INTENT_SYNC_MIN_INTERVAL - millisecondsSinceLastSync) / 1000, 1) // getQuantityString needs an int val remaining = min(Int.MAX_VALUE.toLong(), remainingTimeInSeconds).toInt() - val message = res.getQuantityString( - R.plurals.sync_automatic_sync_needs_more_time, - remaining, - remaining - ) + val message = + res.getQuantityString( + R.plurals.sync_automatic_sync_needs_more_time, + remaining, + remaining, + ) deckPicker.showSimpleNotification(err, message, Channel.SYNC) } else { deckPicker.showSimpleNotification( err, res.getString(R.string.youre_offline), - Channel.SYNC + Channel.SYNC, ) } } @@ -280,8 +299,9 @@ class IntentHandler : Activity() { override fun toMessage(): Message = emptyMessage(this.what) companion object { - const val INTENT_SYNC_MIN_INTERVAL = ( - 2 * 60000 // 2min minimum sync interval + const val INTENT_SYNC_MIN_INTERVAL = + ( + 2 * 60000 // 2min minimum sync interval ).toLong() } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt index 020802b03a66..7be50124249d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt @@ -39,16 +39,16 @@ import timber.log.Timber // TODO: Background of introduction_layout does not display on API 25 emulator: https://github.com/ankidroid/Anki-Android/pull/12033#issuecomment-1228429130 @NeedsTest("Ensure that we can get here on first run without an exception dialog shown") class IntroductionActivity : AnkiActivity() { - @NeedsTest("ensure this is called when the activity ends") - private val onLoginResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == RESULT_OK) { - Timber.i("login successful, opening deck picker to sync") - startDeckPicker(RESULT_SYNC_PROFILE) - } else { - Timber.i("login was not successful") + private val onLoginResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == RESULT_OK) { + Timber.i("login successful, opening deck picker to sync") + startDeckPicker(RESULT_SYNC_PROFILE) + } else { + Timber.i("login was not successful") + } } - } override fun onCreate(savedInstanceState: Bundle?) { if (showedActivityFailedScreen(savedInstanceState)) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptTTS.kt b/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptTTS.kt index f86d0b02b845..ad9ad6b10d42 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptTTS.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptTTS.kt @@ -37,7 +37,9 @@ class JavaScriptTTS internal constructor() : OnInitListener { annotation class TTSLangResult /** OnInitListener method to receive the TTS engine status */ - override fun onInit(@ErrorOrSuccess status: Int) { + override fun onInit( + @ErrorOrSuccess status: Int, + ) { mTtsOk = status == TextToSpeech.SUCCESS } @@ -48,7 +50,10 @@ class JavaScriptTTS internal constructor() : OnInitListener { * @return ERROR(-1) SUCCESS(0) */ @ErrorOrSuccess - fun speak(text: String?, @QueueMode queueMode: Int): Int { + fun speak( + text: String?, + @QueueMode queueMode: Int, + ): Int { return mTts.speak(text, queueMode, mTtsParams, "stringId") } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/LeakCanaryConfiguration.kt b/AnkiDroid/src/main/java/com/ichi2/anki/LeakCanaryConfiguration.kt index 3293de55010f..ec1130dbf285 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/LeakCanaryConfiguration.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/LeakCanaryConfiguration.kt @@ -28,20 +28,24 @@ object LeakCanaryConfiguration { * Disable LeakCanary. */ fun disable() { - config = config.copy( - dumpHeap = false, - retainedVisibleThreshold = 0, - referenceMatchers = AndroidReferenceMatchers.appDefaults, - computeRetainedHeapSize = false, - maxStoredHeapDumps = 0 - ) + config = + config.copy( + dumpHeap = false, + retainedVisibleThreshold = 0, + referenceMatchers = AndroidReferenceMatchers.appDefaults, + computeRetainedHeapSize = false, + maxStoredHeapDumps = 0, + ) } /** * Sets the initial configuration for LeakCanary. This method can be used to match known library * leaks or leaks which have been already reported previously. */ - fun setInitialConfigFor(application: Application, knownMemoryLeaks: List = emptyList()) { + fun setInitialConfigFor( + application: Application, + knownMemoryLeaks: List = emptyList(), + ) { config = config.copy(referenceMatchers = AndroidReferenceMatchers.appDefaults + knownMemoryLeaks) // AppWatcher manual install if not already installed if (!isInstalled) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/LoginActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/LoginActivity.kt index f4dff2b71497..aab5249f8678 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/LoginActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/LoginActivity.kt @@ -46,7 +46,6 @@ import timber.log.Timber */ @NeedsTest("14650: collection permissions are required for this screen to be usable") class LoginActivity : MyAccount(), CollectionPermissionScreenLauncher { - override val permissionScreenLauncher = recreateActivityResultLauncher() override fun onCreate(savedInstanceState: Bundle?) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt b/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt index 22461cbb634b..8167a4c9acc7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt @@ -49,11 +49,12 @@ class MediaRegistration(private val context: Context) { val filename = getFileName(context.contentResolver, uri) val fd = openInputStreamWithURI(uri) val fileNameAndExtension = getFileNameAndExtension(filename) - fileName = if (checkFilename(fileNameAndExtension!!)) { - "${fileNameAndExtension.key}-name" - } else { - fileNameAndExtension.key - } + fileName = + if (checkFilename(fileNameAndExtension!!)) { + "${fileNameAndExtension.key}-name" + } else { + fileNameAndExtension.key + } var clipCopy: File var bytesWritten: Long openInputStreamWithURI(uri).use { copyFd -> @@ -109,7 +110,10 @@ class MediaRegistration(private val context: Context) { return true // successful conversion to jpg. } - private fun shouldConvertToJPG(fileNameExtension: String, fileStream: InputStream): Boolean { + private fun shouldConvertToJPG( + fileNameExtension: String, + fileStream: InputStream, + ): Boolean { if (".jpg" == fileNameExtension) { return false // we are already a jpg, no conversion } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/MetaDB.kt b/AnkiDroid/src/main/java/com/ichi2/anki/MetaDB.kt index 226b7b200310..83a456f1d177 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/MetaDB.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/MetaDB.kt @@ -42,13 +42,14 @@ object MetaDB { @KotlinCleanup("scope function or lateinit db") private fun openDB(context: Context) { try { - mMetaDb = context.openOrCreateDatabase(DATABASE_NAME, 0, null).let { - if (it.needUpgrade(DATABASE_VERSION)) { - upgradeDB(it, DATABASE_VERSION) - } else { - it + mMetaDb = + context.openOrCreateDatabase(DATABASE_NAME, 0, null).let { + if (it.needUpgrade(DATABASE_VERSION)) { + upgradeDB(it, DATABASE_VERSION) + } else { + it + } } - } Timber.v("Opening MetaDB") } catch (e: Exception) { Timber.e(e, "Error opening MetaDB ") @@ -56,7 +57,10 @@ object MetaDB { } /** Creating any table that missing and upgrading necessary tables. */ - private fun upgradeDB(metaDb: SQLiteDatabase, databaseVersion: Int): SQLiteDatabase { + private fun upgradeDB( + metaDb: SQLiteDatabase, + databaseVersion: Int, + ): SQLiteDatabase { Timber.i("MetaDB:: Upgrading Internal Database..") // if (mMetaDb.getVersion() == 0) { Timber.i("MetaDB:: Applying changes for version: 0") @@ -68,11 +72,11 @@ object MetaDB { // Create tables if not exist metaDb.execSQL( "CREATE TABLE IF NOT EXISTS languages (" + " _id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "did INTEGER NOT NULL, ord INTEGER, " + "qa INTEGER, " + "language TEXT)" + "did INTEGER NOT NULL, ord INTEGER, " + "qa INTEGER, " + "language TEXT)", ) metaDb.execSQL( "CREATE TABLE IF NOT EXISTS smallWidgetStatus (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "due INTEGER NOT NULL, eta INTEGER NOT NULL)" + "due INTEGER NOT NULL, eta INTEGER NOT NULL)", ) updateWidgetStatus(metaDb) updateWhiteboardState(metaDb) @@ -86,7 +90,7 @@ object MetaDB { if (columnCount <= 0) { metaDb.execSQL( "CREATE TABLE IF NOT EXISTS whiteboardState (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "did INTEGER NOT NULL, state INTEGER, visible INTEGER, lightpencolor INTEGER, darkpencolor INTEGER, stylus INTEGER)" + "did INTEGER NOT NULL, state INTEGER, visible INTEGER, lightpencolor INTEGER, darkpencolor INTEGER, stylus INTEGER)", ) return } @@ -118,7 +122,7 @@ object MetaDB { metaDb.execSQL( "CREATE TABLE IF NOT EXISTS widgetStatus (" + "deckId INTEGER NOT NULL PRIMARY KEY, " + "deckName TEXT NOT NULL, " + "newCards INTEGER NOT NULL, " + "lrnCards INTEGER NOT NULL, " + - "dueCards INTEGER NOT NULL, " + "progress INTEGER NOT NULL, " + "eta INTEGER NOT NULL)" + "dueCards INTEGER NOT NULL, " + "progress INTEGER NOT NULL, " + "eta INTEGER NOT NULL)", ) } } @@ -203,7 +207,13 @@ object MetaDB { * [.LANGUAGES_QA_ANSWER], or [.LANGUAGES_QA_UNDEFINED] * @param language the language to associate, as a two-characters, lowercase string */ - fun storeLanguage(context: Context, did: DeckId, ord: Int, qa: SoundSide, language: String) { + fun storeLanguage( + context: Context, + did: DeckId, + ord: Int, + qa: SoundSide, + language: String, + ) { openDBIfClosed(context) try { if ("" == getLanguage(context, did, ord, qa)) { @@ -213,8 +223,8 @@ object MetaDB { did, ord, qa.int, - language - ) + language, + ), ) Timber.v("Store language for deck %d", did) } else { @@ -224,8 +234,8 @@ object MetaDB { language, did, ord, - qa.int - ) + qa.int, + ), ) Timber.v("Update language for deck %d", did) } @@ -241,7 +251,12 @@ object MetaDB { * [.LANGUAGES_QA_ANSWER], or [.LANGUAGES_QA_UNDEFINED] return the language associate with * the type, as a two-characters, lowercase string, or the empty string if no association is defined */ - fun getLanguage(context: Context, did: DeckId, ord: Int, qa: SoundSide): String { + fun getLanguage( + context: Context, + did: DeckId, + ord: Int, + qa: SoundSide, + ): String { openDBIfClosed(context) var language = "" val query = "SELECT language FROM languages WHERE did = ? AND ord = ? AND qa = ? LIMIT 1" @@ -251,8 +266,8 @@ object MetaDB { arrayOf( java.lang.Long.toString(did), Integer.toString(ord), - Integer.toString(qa.int) - ) + Integer.toString(qa.int), + ), ).use { cur -> Timber.v("getLanguage: %s", query) if (cur.moveToNext()) { @@ -270,12 +285,15 @@ object MetaDB { * * @return 1 if the whiteboard should be shown, 0 otherwise */ - fun getWhiteboardState(context: Context, did: DeckId): Boolean { + fun getWhiteboardState( + context: Context, + did: DeckId, + ): Boolean { openDBIfClosed(context) try { mMetaDb!!.rawQuery( "SELECT state FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } } catch (e: Exception) { Timber.e(e, "Error retrieving whiteboard state from MetaDB ") @@ -289,25 +307,29 @@ object MetaDB { * @param did deck id to store whiteboard state for * @param whiteboardState 1 if the whiteboard should be shown, 0 otherwise */ - fun storeWhiteboardState(context: Context, did: DeckId, whiteboardState: Boolean) { + fun storeWhiteboardState( + context: Context, + did: DeckId, + whiteboardState: Boolean, + ) { val state = if (whiteboardState) 1 else 0 openDBIfClosed(context) try { val metaDb = mMetaDb!! metaDb.rawQuery( "SELECT _id FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> if (cur.moveToNext()) { metaDb.execSQL( "UPDATE whiteboardState SET did = ?, state=? WHERE _id=?;", - arrayOf(did, state, cur.getString(0)) + arrayOf(did, state, cur.getString(0)), ) Timber.d("Store whiteboard state (%d) for deck %d", state, did) } else { metaDb.execSQL( "INSERT INTO whiteboardState (did, state) VALUES (?, ?)", - arrayOf(did, state) + arrayOf(did, state), ) Timber.d("Store whiteboard state (%d) for deck %d", state, did) } @@ -322,12 +344,15 @@ object MetaDB { * * @return true if the whiteboard stylus mode should be enabled, false otherwise */ - fun getWhiteboardStylusState(context: Context, did: DeckId): Boolean { + fun getWhiteboardStylusState( + context: Context, + did: DeckId, + ): Boolean { openDBIfClosed(context) try { mMetaDb!!.rawQuery( "SELECT stylus FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } } catch (e: Exception) { Timber.e(e, "Error retrieving whiteboard stylus mode state from MetaDB ") @@ -341,25 +366,29 @@ object MetaDB { * @param did deck id to store whiteboard stylus mode state for * @param whiteboardStylusState true if the whiteboard stylus mode should be enabled, false otherwise */ - fun storeWhiteboardStylusState(context: Context, did: DeckId, whiteboardStylusState: Boolean) { + fun storeWhiteboardStylusState( + context: Context, + did: DeckId, + whiteboardStylusState: Boolean, + ) { val state = if (whiteboardStylusState) 1 else 0 openDBIfClosed(context) try { val metaDb = mMetaDb!! metaDb.rawQuery( "SELECT _id FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> if (cur.moveToNext()) { metaDb.execSQL( "UPDATE whiteboardState SET did = ?, stylus=? WHERE _id=?;", - arrayOf(did, state, cur.getString(0)) + arrayOf(did, state, cur.getString(0)), ) Timber.d("Store whiteboard stylus mode state (%d) for deck %d", state, did) } else { metaDb.execSQL( "INSERT INTO whiteboardState (did, stylus) VALUES (?, ?)", - arrayOf(did, state) + arrayOf(did, state), ) Timber.d("Store whiteboard stylus mode state (%d) for deck %d", state, did) } @@ -374,12 +403,15 @@ object MetaDB { * * @return 1 if the whiteboard should be shown, 0 otherwise */ - fun getWhiteboardVisibility(context: Context, did: DeckId): Boolean { + fun getWhiteboardVisibility( + context: Context, + did: DeckId, + ): Boolean { openDBIfClosed(context) try { mMetaDb!!.rawQuery( "SELECT visible FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } } catch (e: Exception) { Timber.e(e, "Error retrieving whiteboard state from MetaDB ") @@ -393,25 +425,29 @@ object MetaDB { * @param did deck id to store whiteboard state for * @param isVisible 1 if the whiteboard should be shown, 0 otherwise */ - fun storeWhiteboardVisibility(context: Context, did: DeckId, isVisible: Boolean) { + fun storeWhiteboardVisibility( + context: Context, + did: DeckId, + isVisible: Boolean, + ) { val isVisibleState = if (isVisible) 1 else 0 openDBIfClosed(context) try { val metaDb = mMetaDb!! metaDb.rawQuery( "SELECT _id FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> if (cur.moveToNext()) { metaDb.execSQL( "UPDATE whiteboardState SET did = ?, visible= ? WHERE _id=?;", - arrayOf(did, isVisibleState, cur.getString(0)) + arrayOf(did, isVisibleState, cur.getString(0)), ) Timber.d("Store whiteboard visibility (%d) for deck %d", isVisibleState, did) } else { metaDb.execSQL( "INSERT INTO whiteboardState (did, visible) VALUES (?, ?)", - arrayOf(did, isVisibleState) + arrayOf(did, isVisibleState), ) Timber.d("Store whiteboard visibility (%d) for deck %d", isVisibleState, did) } @@ -424,12 +460,15 @@ object MetaDB { /** * Returns the pen color of the whiteboard for the given deck. */ - fun getWhiteboardPenColor(context: Context, did: DeckId): WhiteboardPenColor { + fun getWhiteboardPenColor( + context: Context, + did: DeckId, + ): WhiteboardPenColor { openDBIfClosed(context) try { mMetaDb!!.rawQuery( "SELECT lightpencolor, darkpencolor FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> cur.moveToFirst() val light = DatabaseUtil.getInteger(cur, 0) @@ -449,21 +488,26 @@ object MetaDB { * @param isLight if dark mode is disabled * @param value The new color code to store */ - fun storeWhiteboardPenColor(context: Context, did: DeckId, isLight: Boolean, value: Int?) { + fun storeWhiteboardPenColor( + context: Context, + did: DeckId, + isLight: Boolean, + value: Int?, + ) { openDBIfClosed(context) val columnName = if (isLight) "lightpencolor" else "darkpencolor" try { val metaDb = mMetaDb!! metaDb.rawQuery( "SELECT _id FROM whiteboardState WHERE did = ?", - arrayOf(java.lang.Long.toString(did)) + arrayOf(java.lang.Long.toString(did)), ).use { cur -> if (cur.moveToNext()) { metaDb.execSQL( "UPDATE whiteboardState SET did = ?, " + columnName + "= ? " + " WHERE _id=?;", - arrayOf(did, value, cur.getString(0)) + arrayOf(did, value, cur.getString(0)), ) } else { val sql = "INSERT INTO whiteboardState (did, $columnName) VALUES (?, ?)" @@ -491,7 +535,7 @@ object MetaDB { null, null, null, - null + null, ).use { cursor -> if (cursor.moveToNext()) { return intArrayOf(cursor.getInt(0), cursor.getInt(1)) @@ -518,7 +562,10 @@ object MetaDB { return due } - fun storeSmallWidgetStatus(context: Context, status: SmallWidgetStatus) { + fun storeSmallWidgetStatus( + context: Context, + status: SmallWidgetStatus, + ) { openDBIfClosed(context) try { val metaDb = mMetaDb!! @@ -528,7 +575,7 @@ object MetaDB { metaDb.execSQL("DELETE FROM smallWidgetStatus") metaDb.execSQL( "INSERT INTO smallWidgetStatus(due, eta) VALUES (?, ?)", - arrayOf(status.due, status.eta) + arrayOf(status.due, status.eta), ) metaDb.setTransactionSuccessful() } finally { @@ -563,12 +610,17 @@ object MetaDB { } // API LEVEL - fun getTableColumnCount(metaDb: SQLiteDatabase, tableName: String) = - metaDb.rawQuery("PRAGMA table_info($tableName)", null).use { c -> - c.count - } + fun getTableColumnCount( + metaDb: SQLiteDatabase, + tableName: String, + ) = metaDb.rawQuery("PRAGMA table_info($tableName)", null).use { c -> + c.count + } - fun getInteger(cur: Cursor, columnIndex: Int): Int? { + fun getInteger( + cur: Cursor, + columnIndex: Int, + ): Int? { return if (cur.isNull(columnIndex)) null else cur.getInt(columnIndex) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt index 64f85eb75734..f53461d3d997 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt @@ -108,6 +108,7 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { // ---------------------------------------------------------------------------- // UI SETUP // ---------------------------------------------------------------------------- + /** * Initialize the data holding properties and the UI from the model. This method expects that it * isn't followed by other type of work that access the data properties as it has the capability @@ -125,22 +126,25 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { mNoteFields = mNotetype.getJSONArray("flds") mFieldsLabels = mNoteFields.toStringList("name") mFieldsListView.adapter = ArrayAdapter(this, R.layout.model_field_editor_list_item, mFieldsLabels) - mFieldsListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position: Int, _ -> - showDialogFragment(newInstance(mFieldsLabels[position])) - currentPos = position - } + mFieldsListView.onItemClickListener = + AdapterView.OnItemClickListener { _, _, position: Int, _ -> + showDialogFragment(newInstance(mFieldsLabels[position])) + currentPos = position + } } // ---------------------------------------------------------------------------- // CONTEXT MENU DIALOGUES // ---------------------------------------------------------------------------- + /** * Clean the input field or explain why it's rejected * @param fieldNameInput Editor to get the input * @return The value to use, or null in case of failure */ private fun uniqueName(fieldNameInput: EditText): String? { - var input = fieldNameInput.text.toString() - .replace("[\\n\\r{}:\"]".toRegex(), "") + var input = + fieldNameInput.text.toString() + .replace("[\\n\\r{}:\"]".toRegex(), "") // The number of #, ^, /, space, tab, starting the input var offset = 0 while (offset < input.length) { @@ -162,12 +166,13 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { } /* - * Creates a dialog to create a field - */ + * Creates a dialog to create a field + */ private fun addFieldDialog() { - fieldNameInput = FixedEditText(this).apply { - focusWithKeyboard() - } + fieldNameInput = + FixedEditText(this).apply { + focusWithKeyboard() + } fieldNameInput?.let { _fieldNameInput -> _fieldNameInput.isSingleLine = true AlertDialog.Builder(this).show { @@ -184,14 +189,15 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { // Create dialogue to for schema change val c = ConfirmationDialog() c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = Runnable { - try { - addField(fieldName, false) - } catch (e1: ConfirmModSchemaException) { - e1.log() - // This should never be thrown + val confirm = + Runnable { + try { + addField(fieldName, false) + } catch (e1: ConfirmModSchemaException) { + e1.log() + // This should never be thrown + } } - } c.setConfirm(confirm) this@ModelFieldEditor.showDialogFragment(c) } @@ -207,7 +213,10 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { * Adds a field with the given name */ @Throws(ConfirmModSchemaException::class) - private fun addField(fieldName: String?, modSchemaCheck: Boolean) { + private fun addField( + fieldName: String?, + modSchemaCheck: Boolean, + ) { fieldName ?: return // Name is valid, now field is added if (modSchemaCheck) { @@ -230,13 +239,14 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { * Creates a dialog to delete the currently selected field */ private fun deleteFieldDialog() { - val confirm = Runnable { - collection.modSchemaNoCheck() - deleteField() + val confirm = + Runnable { + collection.modSchemaNoCheck() + deleteField() - // This ensures that the context menu closes after the field has been deleted - supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - } + // This ensures that the context menu closes after the field has been deleted + supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } if (mFieldsLabels.size < 2) { showThemedToast(this, resources.getString(R.string.toast_last_field), true) @@ -263,16 +273,17 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { launchCatchingTask { Timber.d("doInBackGroundDeleteField") withProgress(message = getString(R.string.model_field_editor_changing)) { - val result = withCol { - try { - notetypes.remField(mNotetype, mNoteFields.getJSONObject(currentPos)) - true - } catch (e: ConfirmModSchemaException) { - // Should never be reached - e.log() - false + val result = + withCol { + try { + notetypes.remField(mNotetype, mNoteFields.getJSONObject(currentPos)) + true + } catch (e: ConfirmModSchemaException) { + // Should never be reached + e.log() + false + } } - } if (!result) { closeActivity() } @@ -307,15 +318,16 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { // Handler mod schema confirmation val c = ConfirmationDialog() c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = Runnable { - collection.modSchemaNoCheck() - try { - renameField() - } catch (e1: ConfirmModSchemaException) { - e1.log() - // This should never be thrown + val confirm = + Runnable { + collection.modSchemaNoCheck() + try { + renameField() + } catch (e1: ConfirmModSchemaException) { + e1.log() + // This should never be thrown + } } - } c.setConfirm(confirm) this@ModelFieldEditor.showDialogFragment(c) } @@ -339,13 +351,14 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { title(text = String.format(resources.getString(R.string.model_field_editor_reposition), 1, mFieldsLabels.size)) positiveButton(R.string.dialog_ok) { val newPosition = _fieldNameInput.text.toString() - val pos: Int = try { - newPosition.toInt() - } catch (n: NumberFormatException) { - Timber.w(n) - _fieldNameInput.error = resources.getString(R.string.toast_out_of_range) - return@positiveButton - } + val pos: Int = + try { + newPosition.toInt() + } catch (n: NumberFormatException) { + Timber.w(n) + _fieldNameInput.error = resources.getString(R.string.toast_out_of_range) + return@positiveButton + } if (pos < 1 || pos > mFieldsLabels.size) { _fieldNameInput.error = resources.getString(R.string.toast_out_of_range) } else { @@ -359,14 +372,15 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { // Handle mod schema confirmation val c = ConfirmationDialog() c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = Runnable { - try { - collection.modSchemaNoCheck() - repositionField(pos - 1) - } catch (e1: JSONException) { - throw RuntimeException(e1) + val confirm = + Runnable { + try { + collection.modSchemaNoCheck() + repositionField(pos - 1) + } catch (e1: JSONException) { + throw RuntimeException(e1) + } } - } c.setConfirm(confirm) this@ModelFieldEditor.showDialogFragment(c) } @@ -380,17 +394,18 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { private fun repositionField(index: Int) { launchCatchingTask { withProgress(message = getString(R.string.model_field_editor_changing)) { - val result = withCol { - Timber.d("doInBackgroundRepositionField") - try { - notetypes.moveField(mNotetype, mNoteFields.getJSONObject(currentPos), index) - true - } catch (e: ConfirmModSchemaException) { - e.log() - // Should never be reached - false + val result = + withCol { + Timber.d("doInBackgroundRepositionField") + try { + notetypes.moveField(mNotetype, mNoteFields.getJSONObject(currentPos), index) + true + } catch (e: ConfirmModSchemaException) { + e.log() + // Should never be reached + false + } } - } if (!result) { closeActivity() } @@ -404,8 +419,9 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { */ @Throws(ConfirmModSchemaException::class) private fun renameField() { - val fieldLabel = fieldNameInput!!.text.toString() - .replace("[\\n\\r]".toRegex(), "") + val fieldLabel = + fieldNameInput!!.text.toString() + .replace("[\\n\\r]".toRegex(), "") val field = mNoteFields.getJSONObject(currentPos) collection.notetypes.renameField(mNotetype, field, fieldLabel) initialize() @@ -423,16 +439,20 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { // Handler mMod schema confirmation val c = ConfirmationDialog() c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = Runnable { - collection.modSchemaNoCheck() - launchCatchingTask { changeSortField(mNotetype, currentPos) } - } + val confirm = + Runnable { + collection.modSchemaNoCheck() + launchCatchingTask { changeSortField(mNotetype, currentPos) } + } c.setConfirm(confirm) this@ModelFieldEditor.showDialogFragment(c) } } - private suspend fun changeSortField(notetype: NotetypeJson, idx: Int) { + private suspend fun changeSortField( + notetype: NotetypeJson, + idx: Int, + ) { withProgress(resources.getString(R.string.model_field_editor_changing)) { CollectionManager.withCol { Timber.d("doInBackgroundChangeSortField") @@ -452,13 +472,14 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { field.put("sticky", !field.getBoolean("sticky")) } - override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { - R.id.action_add_new_model -> { - addFieldDialog() - true + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + R.id.action_add_new_model -> { + addFieldDialog() + true + } + else -> super.onOptionsItemSelected(item) } - else -> super.onOptionsItemSelected(item) - } private fun closeActivity() { finish() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/MyAccount.kt b/AnkiDroid/src/main/java/com/ichi2/anki/MyAccount.kt index 79ed0f9226ed..90e88a9579d8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/MyAccount.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/MyAccount.kt @@ -45,6 +45,7 @@ open class MyAccount : AnkiActivity() { var toolbar: Toolbar? = null private lateinit var mPasswordLayout: TextInputLayout private lateinit var mAnkidroidLogo: ImageView + open fun switchToState(newState: Int) { when (newState) { STATE_LOGGED_IN -> { @@ -154,7 +155,7 @@ open class MyAccount : AnkiActivity() { } } false - } + }, ) val loginButton = mLoginToMyAccountView.findViewById