diff --git a/.editorconfig b/.editorconfig index 76804a3471ae..a70cd51b14d4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,13 +4,7 @@ end_of_line = lf [*.bat] end_of_line = crlf -# These can be removed once we upgrade to ktlint 1.5.0 -# These are added in preparation for the move to gradle.ktlint 12.1.1 - -# Rule 'standard:no-unused-imports' throws exception in file 'CardBrowser.kt' at position (1228:9) -[*CardBrowser.kt] -ktlint_standard_no-unused-imports = disabled - -# Rule 'standard:parameter-list-wrapping' throws exception in file 'DeckConfig.kt' at position (0:0) -[*DeckConfig.kt] -ktlint_standard_parameter-list-wrapping = disabled \ No newline at end of file +[*.kt] +# We often have block comments representing implementation details after KDocs. +# This feels like an acceptable commenting strategy +ktlint_standard_no-consecutive-comments = 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 c21df701b9e3..0ce9bdbc0f6a 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt @@ -68,7 +68,7 @@ class DeckPickerTest : InstrumentedTest() { // Check if currently open Activity is StudyOptionsActivity assertThat( activityInstance, - instanceOf(StudyOptionsActivity::class.java) + instanceOf(StudyOptionsActivity::class.java), ) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DownloadFileTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DownloadFileTest.kt index 215f1d6f1c94..eea3618dfe75 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DownloadFileTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DownloadFileTest.kt @@ -32,16 +32,17 @@ class DownloadFileTest : InstrumentedTest() { // https://github.com/ankidroid/Anki-Android/issues/17573 // https://issuetracker.google.com/issues/382864232 - val downloadFile = DownloadFile( - url = "https://ankiweb.net/svc/shared/download-deck/293204297?t=token", - userAgent = "unused", - contentDisposition = "attachment; filename=Goethe_Institute_A1_Wordlist.apkg", - mimeType = "application/octet-stream" - ) + val downloadFile = + DownloadFile( + url = "https://ankiweb.net/svc/shared/download-deck/293204297?t=token", + userAgent = "unused", + contentDisposition = "attachment; filename=Goethe_Institute_A1_Wordlist.apkg", + mimeType = "application/octet-stream", + ) assertThat( downloadFile.toFileName(extension = "apkg"), - equalTo("Goethe_Institute_A1_Wordlist.apkg") + equalTo("Goethe_Institute_A1_Wordlist.apkg"), ) } } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/FieldEditLineTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/FieldEditLineTest.kt index 7e63e70b1d6d..df5d3e966af7 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/NoteEditorIntentTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorIntentTest.kt index bfaf1b0d78e5..7bf82bc5c452 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorIntentTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorIntentTest.kt @@ -42,9 +42,10 @@ class NoteEditorIntentTest : InstrumentedTest() { var runtimePermissionRule: TestRule? = GrantStoragePermission.instance @get:Rule - var activityRuleIntent: ActivityScenarioRule? = ActivityScenarioRule( - noteEditorTextIntent - ) + var activityRuleIntent: ActivityScenarioRule? = + ActivityScenarioRule( + noteEditorTextIntent, + ) @Test @Flaky(OS.ALL, "Issue 15707 - java.lang.ArrayIndexOutOfBoundsException: length=0; index=0") @@ -63,10 +64,11 @@ class NoteEditorIntentTest : InstrumentedTest() { @Test fun intentLaunchedWithNonImageIntent() { - val intent = Intent().apply { - action = Intent.ACTION_SEND - type = TEXT_PLAIN - } + val intent = + Intent().apply { + action = Intent.ACTION_SEND + type = TEXT_PLAIN + } assertFalse(intentLaunchedWithImage(intent)) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt index 19d1f159793c..a466410f80f0 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt @@ -41,7 +41,7 @@ class NoteEditorTabOrderTest : NoteEditorTest() { java.lang.AssertionError: - Expected: is "a"""" + Expected: is "a"""", ) @Throws(Throwable::class) fun testTabOrder() { @@ -63,7 +63,10 @@ class NoteEditorTabOrderTest : NoteEditorTest() { } } - private fun sendKeyDownUp(editor: NoteEditor, keyCode: Int) { + private fun sendKeyDownUp( + editor: NoteEditor, + keyCode: Int, + ) { val focusedView = editor.requireActivity().currentFocus ?: return val inputConnection = BaseInputConnection(focusedView, true) inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt index f5c9dd9770a5..5bd532af325b 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt @@ -35,9 +35,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() { @@ -51,8 +52,8 @@ abstract class NoteEditorTest protected constructor() { "Test fails on Travis API $invalid", Build.VERSION.SDK_INT, not( - equalTo(invalid) - ) + equalTo(invalid), + ), ) } } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt index 8471861b40b3..755d96c36352 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/PagesTest.kt @@ -66,44 +66,49 @@ class PagesTest : InstrumentedTest() { @JvmStatic // required for initParameters fun initParameters(): Collection> { /** See [PageFragment] */ - val intents = listOf Intent, String>>( - Pair(PagesTest::getStatistics, "Statistics"), - Pair(PagesTest::getCardInfo, "CardInfo"), - Pair(PagesTest::getCongratsPage, "CongratsPage"), - Pair(PagesTest::getDeckOptions, "DeckOptions"), - // the following need a file path - Pair(PagesTest::needsPath, "AnkiPackageImporterFragment"), - Pair(PagesTest::needsPath, "CsvImporter"), - Pair(PagesTest::needsPath, "ImageOcclusion") - ) + val intents = + listOf Intent, String>>( + Pair(PagesTest::getStatistics, "Statistics"), + Pair(PagesTest::getCardInfo, "CardInfo"), + Pair(PagesTest::getCongratsPage, "CongratsPage"), + Pair(PagesTest::getDeckOptions, "DeckOptions"), + // the following need a file path + Pair(PagesTest::needsPath, "AnkiPackageImporterFragment"), + Pair(PagesTest::needsPath, "CsvImporter"), + Pair(PagesTest::needsPath, "ImageOcclusion"), + ) return intents.map { arrayOf(it.first, it.second) } } } } -fun PagesTest.getStatistics(context: Context): Intent { - return Statistics.getIntent(context) -} +fun PagesTest.getStatistics(context: Context): Intent = Statistics.getIntent(context) -fun PagesTest.getCardInfo(context: Context): Intent { - return addNoteUsingBasicModel().firstCard(col).let { card -> +fun PagesTest.getCardInfo(context: Context): Intent = + addNoteUsingBasicModel().firstCard(col).let { card -> this.card = card CardInfoDestination(card.id).toIntent(context) } -} -fun PagesTest.getCongratsPage(context: Context): Intent { - return addNoteUsingBasicModel().firstCard(col).let { card -> +fun PagesTest.getCongratsPage(context: Context): Intent = + addNoteUsingBasicModel().firstCard(col).let { card -> this.card = card CardInfoDestination(card.id).toIntent(context) } -} -fun PagesTest.getDeckOptions(context: Context): Intent { - return DeckOptions.getIntent(context, col.decks.allNamesAndIds().first().id) -} -fun PagesTest.needsPath(@Suppress("UNUSED_PARAMETER") context: Context): Intent { +fun PagesTest.getDeckOptions(context: Context): Intent = + DeckOptions.getIntent( + context, + col.decks + .allNamesAndIds() + .first() + .id, + ) + +fun PagesTest.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/ReviewerFragmentTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentTest.kt index 66cae7ddfb48..20fc46f998c8 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentTest.kt @@ -43,7 +43,6 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class ReviewerFragmentTest : 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 @@ -76,9 +75,13 @@ class ReviewerFragmentTest : InstrumentedTest() { card.moveToReviewQueue() col.backend.updateCards( listOf( - card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build() + card + .toBackendCard() + .toBuilder() + .setCustomData("""{"c":1}""") + .build(), ), - true + true, ) closeGetStartedScreenIfExists() @@ -135,7 +138,7 @@ class ReviewerFragmentTest : InstrumentedTest() { 100, // Increase to a max of 30 seconds because CI builds can be very // slow - TimeUnit.SECONDS.toMillis(30) + TimeUnit.SECONDS.toMillis(30), ) } @@ -148,4 +151,6 @@ class ReviewerFragmentTest : InstrumentedTest() { private var Collection.cardStateCustomizer: String? get() = config.get("cardStateCustomizer") - set(value) { config.set("cardStateCustomizer", value) } + set(value) { + config.set("cardStateCustomizer", value) + } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt index f08f1858ce96..1bdd2dc5ed62 100755 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerTest.kt @@ -52,7 +52,6 @@ import java.lang.AssertionError @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 @@ -95,9 +94,13 @@ class ReviewerTest : InstrumentedTest() { card.moveToReviewQueue() col.backend.updateCards( listOf( - card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build() + card + .toBackendCard() + .toBuilder() + .setCustomData("""{"c":1}""") + .build(), ), - true + true, ) closeGetStartedScreenIfExists() @@ -149,8 +152,8 @@ class ReviewerTest : InstrumentedTest() { onView(withId(R.id.decks)).perform( RecyclerViewActions.actionOnItem( hasDescendant(withText(deckName)), - click() - ) + click(), + ), ) } @@ -202,13 +205,13 @@ class ReviewerTest : InstrumentedTest() { // ...on the command line it has resource name "good_button"... onView(withResourceName("good_button")).checkWithTimeout( matches(isDisplayed()), - 100 + 100, ) } catch (e: AssertionError) { // ...but in Android Studio it has resource name "flashcard_layout_ease3" !? onView(withResourceName("flashcard_layout_ease3")).checkWithTimeout( matches(isDisplayed()), - 100 + 100, ) } } @@ -228,4 +231,6 @@ class ReviewerTest : InstrumentedTest() { private var Collection.cardStateCustomizer: String? get() = config.get("cardStateCustomizer") - set(value) { config.set("cardStateCustomizer", value) } + set(value) { + config.set("cardStateCustomizer", value) + } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt index 40e79cade646..1dbf623e45f9 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/TestUtils.kt @@ -27,7 +27,6 @@ import androidx.test.runner.lifecycle.Stage import org.hamcrest.Matcher object TestUtils { - /** * Get instance of current activity */ @@ -37,7 +36,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 @@ -52,31 +51,30 @@ object TestUtils { * so test for that screen layout in our resources configuration */ val isTablet: Boolean - get() = ( - activityInstance!!.resources.configuration.screenLayout and - Configuration.SCREENLAYOUT_SIZE_MASK + get() = + ( + activityInstance!!.resources.configuration.screenLayout and + Configuration.SCREENLAYOUT_SIZE_MASK ) == - Configuration.SCREENLAYOUT_SIZE_XLARGE + Configuration.SCREENLAYOUT_SIZE_XLARGE /** * Click on a view using its ID inside a RecyclerView item */ - fun clickChildViewWithId(id: Int): ViewAction { - return object : ViewAction { - override fun getConstraints(): Matcher? { - return null - } + fun clickChildViewWithId(id: Int): ViewAction = + object : ViewAction { + override fun getConstraints(): Matcher? = null - override fun getDescription(): String { - return "Click on a child view with specified id." - } + override fun getDescription(): String = "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() } } - } /** @return if the instrumented tests were built on a CI machine */ diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/TtsVoicesTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/TtsVoicesTest.kt index 9078e3a907c8..6d2d60926b27 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/TtsVoicesTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/TtsVoicesTest.kt @@ -28,7 +28,10 @@ import java.util.Locale class TtsVoicesTest : InstrumentedTest() { @Test fun normalize() { - fun assertEqual(l: Locale, str: String) { + fun assertEqual( + l: Locale, + str: String, + ) { val normalized = AndroidTtsVoice.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 adbc9a0f4c56..f6149ad3a911 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/ModelEditorContextMenuTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/ModelEditorContextMenuTest.kt @@ -38,7 +38,7 @@ class ModelEditorContextMenuTest : InstrumentedTest() { fun showsAllOptions() { launchFragment( fragmentArgs = bundleOf(ModelEditorContextMenu.KEY_LABEL to testDialogTitle), - themeResId = R.style.Theme_Light + themeResId = R.style.Theme_Light, ) { ModelEditorContextMenu() } onView(withText(testDialogTitle)).check(matches(isDisplayed())) ModelEditorContextMenuAction.entries.forEach { diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/libanki/SoundTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/libanki/SoundTest.kt index 7ab001c82d4f..d88589dc343a 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/libanki/SoundTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/libanki/SoundTest.kt @@ -27,7 +27,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SoundTest : InstrumentedTest() { - @Test fun mp4IsDetected() { val mp4 = Shared.getTestFile(testContext, "anki-15872-valid-1.mp4") 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 72b2fcef9966..c148563b8f8b 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,20 +40,18 @@ 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), ) } - private fun getNumpadEvent(keycode: Int): KeyEvent { - return KeyEvent(0, 0, KeyEvent.ACTION_UP, keycode, 0, KeyEvent.META_NUM_LOCK_ON) - } + private fun getNumpadEvent(keycode: Int): KeyEvent = KeyEvent(0, 0, KeyEvent.ACTION_UP, keycode, 0, KeyEvent.META_NUM_LOCK_ON) } 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 2f68a02f26cd..36f072534d18 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt @@ -74,8 +74,11 @@ class ACRATest : InstrumentedTest() { CrashReportService.setDebugACRAConfig(sharedPrefs) assertArrayEquals( "Debug logcat arguments not set correctly", - CrashReportService.acraCoreConfigBuilder.build().logcatArguments.toTypedArray(), - debugLogcatArguments + CrashReportService.acraCoreConfigBuilder + .build() + .logcatArguments + .toTypedArray(), + debugLogcatArguments, ) verifyDebugACRAPreferences() } @@ -84,13 +87,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"), ) } @@ -136,27 +139,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 @@ -164,8 +169,8 @@ class ACRATest : InstrumentedTest() { LimitingReportAdministrator().shouldSendReport( testContext, CrashReportService.acraCoreConfigBuilder.build(), - crashData - ) + crashData, + ), ) // Now let's clear data @@ -176,8 +181,8 @@ class ACRATest : InstrumentedTest() { LimitingReportAdministrator().shouldSendReport( testContext, CrashReportService.acraCoreConfigBuilder.build(), - crashData - ) + crashData, + ), ) } @@ -241,13 +246,16 @@ class ACRATest : InstrumentedTest() { assertThat("First handler is ThrowableFilterService", firstExceptionHandler is ThrowableFilterService.FilteringExceptionHandler) ThrowableFilterService.unInstallDefaultExceptionHandler() var secondExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() - assertThat("Second handler is AnalyticsLoggingExceptionHandler", secondExceptionHandler is UsageAnalytics.AnalyticsLoggingExceptionHandler) + assertThat( + "Second handler is AnalyticsLoggingExceptionHandler", + secondExceptionHandler is UsageAnalytics.AnalyticsLoggingExceptionHandler, + ) UsageAnalytics.unInstallDefaultExceptionHandler() var thirdExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() assertThat( "Third handler is neither Analytics nor ThrowableFilter", thirdExceptionHandler !is UsageAnalytics.AnalyticsLoggingExceptionHandler && - thirdExceptionHandler !is ThrowableFilterService.FilteringExceptionHandler + thirdExceptionHandler !is ThrowableFilterService.FilteringExceptionHandler, ) // chain them again @@ -261,13 +269,16 @@ class ACRATest : InstrumentedTest() { ThrowableFilterService.unInstallDefaultExceptionHandler() secondExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() Timber.i("Second handler is a %s", secondExceptionHandler) - assertThat("Second handler is AnalyticsLoggingExceptionHandler", secondExceptionHandler is UsageAnalytics.AnalyticsLoggingExceptionHandler) + assertThat( + "Second handler is AnalyticsLoggingExceptionHandler", + secondExceptionHandler is UsageAnalytics.AnalyticsLoggingExceptionHandler, + ) UsageAnalytics.unInstallDefaultExceptionHandler() thirdExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() assertThat( "Third handler is neither Analytics nor ThrowableFilter", thirdExceptionHandler !is UsageAnalytics.AnalyticsLoggingExceptionHandler && - thirdExceptionHandler !is ThrowableFilterService.FilteringExceptionHandler + thirdExceptionHandler !is ThrowableFilterService.FilteringExceptionHandler, ) } @@ -276,7 +287,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 @@ -297,13 +311,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( app!!.resources.getString(res), - (configuration as ToastConfiguration).text + (configuration as ToastConfiguration).text, ) assertTrue("Toast should be enabled", configuration.enabled()) } @@ -313,7 +329,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 0b7a45f3f622..26b72c07d58f 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -112,7 +112,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 @@ -128,9 +129,11 @@ class ContentProviderTest : InstrumentedTest() { } private fun createBasicModel(name: String = BASIC_MODEL_NAME): NotetypeJson { - val m = BackendUtils.fromJsonBytes( - col.getStockNotetypeLegacy(StockNotetype.Kind.KIND_BASIC) - ).apply { set("name", name) } + val m = + BackendUtils + .fromJsonBytes( + col.getStockNotetypeLegacy(StockNotetype.Kind.KIND_BASIC), + ).apply { set("name", name) } col.addNotetypeLegacy(BackendUtils.toJsonBytes(m)) return col.notetypes.byName(name)!! } @@ -153,7 +156,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 @@ -161,7 +164,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that all created decks have been deleted", numDecksBeforeTest, - col.decks.count() + col.decks.count(), ) // Delete test model col.modSchemaNoCheck() @@ -170,7 +173,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) @@ -183,19 +189,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") @@ -214,11 +222,12 @@ class ContentProviderTest : InstrumentedTest() { // Get required objects for test val cr = contentResolver // Add the note - val values = ContentValues().apply { - put(FlashCardsContract.Note.MID, modelId) - put(FlashCardsContract.Note.FLDS, Utils.joinFields(TEST_NOTE_FIELDS)) - put(FlashCardsContract.Note.TAGS, TEST_TAG) - } + val values = + ContentValues().apply { + put(FlashCardsContract.Note.MID, modelId) + 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 @@ -229,7 +238,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(modelId) @@ -250,11 +259,12 @@ class ContentProviderTest : InstrumentedTest() { @Test 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) + } assertThrows { contentResolver.insert(FlashCardsContract.Note.CONTENT_URI, values) } @@ -277,13 +287,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 @@ -292,8 +303,8 @@ class ContentProviderTest : InstrumentedTest() { "Check template uri ord", expectedOrd.toLong(), ContentUris.parseId( - templateUri!! - ) + templateUri!!, + ), ) model = col.notetypes.get(modelId) assertNotNull("Check model", model) @@ -301,12 +312,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")) @@ -344,12 +355,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) } @@ -361,20 +372,21 @@ class ContentProviderTest : InstrumentedTest() { fun testQueryDirectSqlQuery() { // search for correct mid val cr = contentResolver - cr.query( - FlashCardsContract.Note.CONTENT_URI_V2, - null, - "mid=$modelId", - null, - null - ).use { cursor -> - assertNotNull(cursor) - assertEquals( - "Check number of results", - createdNotes.size, - cursor.count - ) - } + cr + .query( + FlashCardsContract.Note.CONTENT_URI_V2, + null, + "mid=$modelId", + null, + null, + ).use { cursor -> + assertNotNull(cursor) + assertEquals( + "Check number of results", + createdNotes.size, + cursor.count, + ) + } // search for bogus mid cr.query(FlashCardsContract.Note.CONTENT_URI_V2, null, "mid=0", null, null).use { cursor -> assertNotNull(cursor) @@ -400,7 +412,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check number of results", createdNotes.size, - it.count + it.count, ) while (it.moveToNext()) { // Check that it's possible to leave out columns from the projection @@ -413,28 +425,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), ) } } @@ -452,38 +464,42 @@ class ContentProviderTest : InstrumentedTest() { // Query all available notes for (i in FlashCardsContract.Note.DEFAULT_PROJECTION.indices) { val projection = removeFromProjection(FlashCardsContract.Note.DEFAULT_PROJECTION, i) - cr.query( - FlashCardsContract.Note.CONTENT_URI, - projection, - "tag:$TEST_TAG", - null, - null - ).use { allNotesCursor -> - assertNotNull("Check that there is a valid cursor", allNotesCursor) - assertEquals( - "Check number of results", - createdNotes.size, - allNotesCursor!!.count - ) - // Check columns - assertEquals( - "Check column count", - projection.size, - allNotesCursor.columnCount - ) - for (j in projection.indices) { + cr + .query( + FlashCardsContract.Note.CONTENT_URI, + projection, + "tag:$TEST_TAG", + null, + null, + ).use { allNotesCursor -> + assertNotNull("Check that there is a valid cursor", allNotesCursor) assertEquals( - "Check column name $j", - projection[j], - allNotesCursor.getColumnName(j) + "Check number of results", + createdNotes.size, + allNotesCursor!!.count, ) + // Check columns + assertEquals( + "Check column count", + projection.size, + allNotesCursor.columnCount, + ) + for (j in projection.indices) { + assertEquals( + "Check column name $j", + projection[j], + allNotesCursor.getColumnName(j), + ) + } } - } } } @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) @@ -509,25 +525,27 @@ class ContentProviderTest : InstrumentedTest() { // Update the flds cv.put(FlashCardsContract.Note.FLDS, Utils.joinFields(dummyFields2)) cr.update(uri, cv, null, null) - cr.query(uri, FlashCardsContract.Note.DEFAULT_PROJECTION, null, null, null) + cr + .query(uri, FlashCardsContract.Note.DEFAULT_PROJECTION, null, null, null) .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(), ) } } @@ -539,12 +557,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) @@ -557,19 +576,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) @@ -577,7 +596,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) @@ -585,21 +604,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) @@ -608,7 +629,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")) @@ -641,22 +662,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 = @@ -666,21 +688,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), ) } } @@ -701,46 +723,48 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check number of results", createdNotes.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 = testDeckIds[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)) @@ -757,26 +781,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, ) } } @@ -795,10 +820,11 @@ class ContentProviderTest : InstrumentedTest() { FlashCardsContract.Note.CONTENT_URI, FlashCardsContract.Model.CONTENT_URI, FlashCardsContract.Deck.CONTENT_ALL_URI, - FlashCardsContract.Note.CONTENT_URI.buildUpon() + FlashCardsContract.Note.CONTENT_URI + .buildUpon() .appendPath("1234") .appendPath("cards") - .build() + .build(), ) for (uri in updateUris) { try { @@ -814,19 +840,22 @@ class ContentProviderTest : InstrumentedTest() { val deleteUris = arrayOf( FlashCardsContract.Note.CONTENT_URI, - FlashCardsContract.Note.CONTENT_URI.buildUpon() + FlashCardsContract.Note.CONTENT_URI + .buildUpon() .appendPath("1234") .appendPath("cards") .build(), - FlashCardsContract.Note.CONTENT_URI.buildUpon() + FlashCardsContract.Note.CONTENT_URI + .buildUpon() .appendPath("1234") .appendPath("cards") .appendPath("2345") .build(), FlashCardsContract.Model.CONTENT_URI, - FlashCardsContract.Model.CONTENT_URI.buildUpon() + FlashCardsContract.Model.CONTENT_URI + .buildUpon() .appendPath("1234") - .build() + .build(), ) for (uri in deleteUris) { try { @@ -839,21 +868,25 @@ class ContentProviderTest : InstrumentedTest() { // Can't do an insert with specific ID on the following tables val insertUris = arrayOf( - FlashCardsContract.Note.CONTENT_URI.buildUpon() + FlashCardsContract.Note.CONTENT_URI + .buildUpon() .appendPath("1234") .build(), - FlashCardsContract.Note.CONTENT_URI.buildUpon() + FlashCardsContract.Note.CONTENT_URI + .buildUpon() .appendPath("1234") .appendPath("cards") .build(), - FlashCardsContract.Note.CONTENT_URI.buildUpon() + FlashCardsContract.Note.CONTENT_URI + .buildUpon() .appendPath("1234") .appendPath("cards") .appendPath("2345") .build(), - FlashCardsContract.Model.CONTENT_URI.buildUpon() + FlashCardsContract.Model.CONTENT_URI + .buildUpon() .appendPath("1234") - .build() + .build(), ) for (uri in insertUris) { try { @@ -873,20 +906,21 @@ class ContentProviderTest : InstrumentedTest() { @Test fun testQueryAllDecks() { 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 = @@ -898,7 +932,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that the received deck has the correct name", deck.getString("name"), - deckName + deckName, ) } } @@ -910,10 +944,11 @@ class ContentProviderTest : InstrumentedTest() { @Test fun testQueryCertainDeck() { val deckId = testDeckIds[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") @@ -926,12 +961,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, ) } } @@ -943,13 +978,14 @@ class ContentProviderTest : InstrumentedTest() { @Test fun testQueryNextCard() { 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() @@ -966,12 +1002,12 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that received card and actual card have same note id", nextCard!!.nid, - noteID + noteID, ) assertEquals( "Check that received card and actual card have same card ord", nextCard.ord, - cardOrd + cardOrd, ) } @@ -987,13 +1023,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 { @@ -1005,7 +1042,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 @@ -1022,12 +1059,12 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that received card and actual card have same note id", nextCard!!.nid, - noteID + noteID, ) assertEquals( "Check that received card and actual card have same card ord", nextCard.ord, - cardOrd + cardOrd, ) } col.decks.select(selectedDeckBeforeTest) @@ -1048,7 +1085,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Check that the selected deck has been correctly set", deckId, - col.decks.selected() + col.decks.selected(), ) } @@ -1073,18 +1110,20 @@ class ContentProviderTest : InstrumentedTest() { val noteId = card.nid val cardOrd = card.ord val earlyGraduatingEase = Ease.EASY - 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.value) - 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.value) + 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) { @@ -1111,7 +1150,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 @@ -1124,11 +1163,12 @@ class ContentProviderTest : InstrumentedTest() { val noteId = card.nid 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) @@ -1139,7 +1179,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals( "Card is user-buried", Consts.QUEUE_TYPE_MANUALLY_BURIED, - cardAfterUpdate.queue + cardAfterUpdate.queue, ) // cleanup, unbury cards @@ -1160,7 +1200,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 @@ -1173,11 +1213,12 @@ class ContentProviderTest : InstrumentedTest() { val noteId = card.nid val cardOrd = card.ord - val values = ContentValues().apply { - put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) - put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) - put(FlashCardsContract.ReviewInfo.SUSPEND, 1) - } + val values = + ContentValues().apply { + put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) + put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd) + put(FlashCardsContract.ReviewInfo.SUSPEND, 1) + } val updateCount = cr.update(reviewInfoUri, values, null, null) assertEquals("Check if update returns 1", 1, updateCount) @@ -1212,10 +1253,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) @@ -1235,7 +1277,7 @@ class ContentProviderTest : InstrumentedTest() { fun testProviderProvidesDefaultForEmptyModelDeck() { assumeTrue( "This causes mild data corruption - should not be run on a collection you care about", - isEmulator() + isEmulator(), ) col.notetypes.all()[0].put("did", JSONObject.NULL) @@ -1270,34 +1312,37 @@ 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, - // 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) + contentResolver + .query( + specificCardUri, + // 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) - assertThat("[sound: tag should remain", question, containsString(sound)) - assertThat("[sound: tag should remain", answer, containsString(sound)) - } ?: fail("query returned null") + assertThat("[sound: tag should remain", question, containsString(sound)) + assertThat("[sound: tag should remain", answer, containsString(sound)) + } ?: fail("query returned null") } private fun reopenCol(): com.ichi2.libanki.Collection { @@ -1316,14 +1361,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") @@ -1338,7 +1384,7 @@ class ContentProviderTest : InstrumentedTest() { mid: Long, did: Long, fields: Array, - tag: String + tag: String, ): Uri { val newNote = Note.fromNotetypeId(col, mid) for (idx in fields.indices) { @@ -1348,7 +1394,7 @@ class ContentProviderTest : InstrumentedTest() { assertThat( "At least one card added for note", col.addNote(newNote), - greaterThanOrEqualTo(1) + greaterThanOrEqualTo(1), ) for (c in newNote.cards(col)) { c.did = did @@ -1356,12 +1402,17 @@ class ContentProviderTest : InstrumentedTest() { } return Uri.withAppendedPath( FlashCardsContract.Note.CONTENT_URI, - newNote.id.toString() + newNote.id.toString(), ) } } - fun addNonClozeModel(name: String, fields: Array, qfmt: String?, afmt: String?): String { + fun addNonClozeModel( + name: String, + fields: Array, + qfmt: String?, + afmt: String?, + ): String { val model = col.notetypes.new(name) for (field in fields) { col.notetypes.addFieldInNewModel(model, col.notetypes.newField(field)) 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 55e8b410887e..8a14a5fd3e67 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt @@ -77,9 +77,10 @@ abstract class InstrumentedTest { * https://github.com/react-native-community/react-native-device-info/blob/bb505716ff50e5900214fcbcc6e6434198010d95/android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java#L185 * @return boolean true if the execution environment is most likely an emulator */ - fun isEmulator(): Boolean { - return ( - Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") || + fun isEmulator(): Boolean = + ( + Build.BRAND.startsWith("generic") && + Build.DEVICE.startsWith("generic") || Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.HARDWARE.contains("goldfish") || @@ -95,8 +96,7 @@ abstract class InstrumentedTest { Build.PRODUCT.contains("vbox86p") || Build.PRODUCT.contains("emulator") || Build.PRODUCT.contains("simulator") - ) - } + ) } @Before @@ -156,14 +156,19 @@ 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 { - return addNoteUsingModelName("Basic", front, back) - } + internal fun addNoteUsingBasicModel( + front: String = "Front", + back: String = "Back", + ): Note = 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) @@ -182,12 +187,12 @@ abstract class InstrumentedTest { fun ViewInteraction.checkWithTimeout( viewAssertion: ViewAssertion, retryWaitTimeInMilliseconds: Long = 100, - maxWaitTimeInMilliseconds: Long = TimeUnit.SECONDS.toMillis(10) + maxWaitTimeInMilliseconds: Long = TimeUnit.SECONDS.toMillis(10), ) { assertThat( "The retry time is greater than the max wait time. You probably gave the argument in the wrong order.", retryWaitTimeInMilliseconds, - lessThanOrEqualTo(maxWaitTimeInMilliseconds) + lessThanOrEqualTo(maxWaitTimeInMilliseconds), ) 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 7abb518be13c..0d8e50417254 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> { @@ -100,10 +100,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 15add046cffd..584eacd6d239 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/NotificationChannelTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/NotificationChannelTest.kt @@ -57,9 +57,7 @@ class NotificationChannelTest : InstrumentedTest() { manager = targetContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } - private fun channelsInAPI(): Boolean { - return currentAPI >= 26 - } + private fun channelsInAPI(): Boolean = currentAPI >= 26 @Test fun testChannelCreation() { @@ -85,12 +83,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", - manager.getNotificationChannel(channel.id) + manager.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 895cef89a253..11dad9f111cb 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt @@ -51,30 +51,33 @@ object Shared { * emptied on every invocation of this method so it is suitable to use at the start of each test. * Only add files (and not subdirectories) to this directory. */ - fun getTestDir(context: Context): File { - return getTestDir(context, "") - } + fun getTestDir(context: Context): File = getTestDir(context, "") /** * @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("failed to make directory '${dir.path}'", 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("failed to delete '${f.path}'", f.delete()) } @@ -89,10 +92,14 @@ object Shared { * system and can not return a usable path, so copying them to disk is a requirement. */ @Throws(IOException::class) - fun getTestFile(context: Context, name: String): File { + fun getTestFile( + context: Context, + name: String, + ): File { assertThat("folders are not yet supported", name, not(containsString("/"))) - val inputStream = context.classLoader.getResourceAsStream("assets/$name") - ?: throw FileNotFoundException("Could not find test file: assets/$name") + val inputStream = + context.classLoader.getResourceAsStream("assets/$name") + ?: throw FileNotFoundException("Could not find test file: assets/$name") val dstFile = File(getTestDir(context, name), name) val dst = dstFile.absolutePath writeToFile(inputStream, dst) @@ -107,7 +114,10 @@ object Shared { * @throws IOException Rethrows exception after a set number of retries */ @Throws(IOException::class) - fun writeToFile(source: InputStream, destination: String) { + fun writeToFile( + source: InputStream, + destination: String, + ) { // sometimes this fails and works on retries (hardware issue?) val retries = 5 var retryCnt = 0 @@ -137,7 +147,10 @@ object Shared { * Throws the exception, so we can report it in syncing log */ @Throws(IOException::class) - private fun writeToFileImpl(source: InputStream, destination: String) { + private fun writeToFileImpl( + source: InputStream, + destination: String, + ) { val f = File(destination) try { Timber.d("Creating new file... = %s", destination) @@ -161,7 +174,7 @@ object Shared { "Utils.writeToFile: Size: %d Kb, Duration: %d s, Speed: %d Kb/s", sizeKb, durationSeconds, - speedKbSec + speedKbSec, ) } catch (e: IOException) { throw IOException(f.name + ": " + e.localizedMessage, e) 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 a37a07d35091..8127d61d52cb 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 @@ -81,8 +82,11 @@ class DBTest : InstrumentedTest() { } // Test fixture that lets us inspect corruption handler status - inner class TestCallback(version: Int) : AnkiSupportSQLiteDatabase.DefaultDbCallback(version) { + 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 646dd9a4dafd..3bb81d610012 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 @@ -134,7 +134,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 9333da1fb7f5..60b51a628153 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 testCol = 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 = testCol.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 877ae519c53e..7b29f78b50da 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 @@ -29,7 +29,9 @@ import kotlin.Throws * @param i how many times to try * @throws IllegalArgumentException if maxTries is less than 1 */ -class RetryRule(i: Int) : TestRule { +class RetryRule( + i: Int, +) : TestRule { /** * How many times to try a test */ @@ -44,11 +46,15 @@ class RetryRule(i: Int) : TestRule { maxTries = i } - override fun apply(base: Statement, description: Description): Statement { - return statement(base, description) - } + override fun apply( + base: Statement, + description: Description, + ): Statement = 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/DeckPicker.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DeckPicker.kt index 34c25ebaabab..777ad605f13d 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DeckPicker.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DeckPicker.kt @@ -72,8 +72,8 @@ fun tapOnCountLayouts(deckName: String) { onView(withId(R.id.decks)).perform( RecyclerViewActions.actionOnItem( hasDescendant(withText(deckName)), - 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. @@ -89,8 +89,8 @@ fun clickOnDeckWithName(deckName: String) { onView(withId(R.id.decks)).perform( RecyclerViewActions.actionOnItem( hasDescendant(withText(deckName)), - click() - ) + click(), + ), ) } 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..70d06ea30a26 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/GrantPermissions.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/GrantPermissions.kt @@ -23,15 +23,19 @@ import com.ichi2.anki.utils.ensureAllFilesAccess 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 - } + 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 + } /** * Storage is longer necessary for API 30+ @@ -40,11 +44,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/testutil/NoteEditor.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/NoteEditor.kt index 8998585e4957..afe00c5cf88b 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/NoteEditor.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/NoteEditor.kt @@ -28,9 +28,7 @@ import java.util.concurrent.atomic.AtomicReference * @throws Throwable if any exception is thrown during the execution of the block. */ @Throws(Throwable::class) -fun ActivityScenario.onNoteEditor( - block: (NoteEditor) -> Unit -) { +fun ActivityScenario.onNoteEditor(block: (NoteEditor) -> Unit) { val wrapped = AtomicReference(null) this.onActivity { activity: SingleFragmentActivity -> try { @@ -52,6 +50,4 @@ fun ActivityScenario.onNoteEditor( /** * Extension function for SingleFragmentActivity to find the NoteEditor fragment */ -fun SingleFragmentActivity.getEditor(): NoteEditor { - return supportFragmentManager.findFragmentById(R.id.fragment_container) as NoteEditor -} +fun SingleFragmentActivity.getEditor(): NoteEditor = supportFragmentManager.findFragmentById(R.id.fragment_container) as NoteEditor diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/TestEnvironment.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/TestEnvironment.kt index c0d97a4e540b..36d1e4310e8c 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/TestEnvironment.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/TestEnvironment.kt @@ -18,7 +18,5 @@ package com.ichi2.anki.testutil import java.util.Locale object TestEnvironment { - fun isDisplayingDefaultEnglishStrings(): Boolean { - return "en-US" == Locale.getDefault().toLanguageTag() - } + fun isDisplayingDefaultEnglishStrings(): Boolean = "en-US" == Locale.getDefault().toLanguageTag() } 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/anki/utils/KeyUtilsTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/utils/KeyUtilsTest.kt index c91ba422060c..85e26630009e 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/utils/KeyUtilsTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/utils/KeyUtilsTest.kt @@ -27,7 +27,6 @@ import kotlin.test.assertNull @RunWith(AndroidJUnit4::class) class KeyUtilsTest { - @Test fun testIsDigitWithValidDigitShouldReturnTrue() { val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5) diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt b/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt index a2050beb1298..b2dd8d099bad 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/testutils/NewCollectionPathTestRunner.kt @@ -26,7 +26,9 @@ import androidx.test.runner.AndroidJUnitRunner */ @Suppress("unused") // referenced by build.gradle class NewCollectionPathTestRunner : AndroidJUnitRunner() { - override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { - return super.newApplication(cl, TestingApplication::class.java.name, context) - } + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context?, + ): Application = super.newApplication(cl, TestingApplication::class.java.name, context) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt b/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt index e497ebfb2b1d..ef03e0eede0a 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) @@ -35,10 +40,37 @@ object ActivityTransitionAnimation { } } - 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) + fun getAnimationOptions( + activity: Activity, + direction: Direction?, + ): ActivityOptionsCompat = + 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.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) @@ -49,15 +81,21 @@ object ActivityTransitionAnimation { ActivityOptionsCompat.makeBasic() else -> ActivityOptionsCompat.makeBasic() } - } - private fun isRightToLeft(c: Context): Boolean { - return c.resources.configuration.layoutDirection == LayoutDirection.RTL - } + private fun isRightToLeft(c: Context): Boolean = c.resources.configuration.layoutDirection == LayoutDirection.RTL @Parcelize enum class Direction : Parcelable { - START, END, FADE, UP, DOWN, RIGHT, LEFT, DEFAULT, NONE; + START, + END, + FADE, + UP, + DOWN, + RIGHT, + LEFT, + DEFAULT, + NONE, + ; /** @see getInverseTransition */ fun invert(): Direction = getInverseTransition(this) @@ -67,8 +105,8 @@ object ActivityTransitionAnimation { * @return inverse transition of [direction] * if there isn't one, return the same [direction] */ - fun getInverseTransition(direction: Direction): Direction { - return when (direction) { + fun getInverseTransition(direction: Direction): Direction = + when (direction) { // Directional transitions which should return their opposites Direction.RIGHT -> Direction.LEFT Direction.LEFT -> Direction.RIGHT @@ -81,5 +119,4 @@ object ActivityTransitionAnimation { Direction.DEFAULT -> Direction.DEFAULT Direction.NONE -> Direction.NONE } - } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 841ba7736aad..9f4caa4b2a83 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -315,16 +315,17 @@ abstract class AbstractFlashcardViewer : private val startLongClickAction = Runnable { gestureProcessor.onLongTap() } // Handler for the "show answer" button - private val flipCardListener = View.OnClickListener { - Timber.i("AbstractFlashcardViewer:: Show answer button pressed") - // Ignore what is most likely an accidental double-tap. - if (elapsedRealTime - lastClickTime < doubleTapTimeInterval) { - return@OnClickListener + private val flipCardListener = + View.OnClickListener { + Timber.i("AbstractFlashcardViewer:: Show answer button pressed") + // Ignore what is most likely an accidental double-tap. + if (elapsedRealTime - lastClickTime < doubleTapTimeInterval) { + return@OnClickListener + } + lastClickTime = elapsedRealTime + automaticAnswer.onShowAnswer() + displayCardAnswer() } - lastClickTime = elapsedRealTime - automaticAnswer.onShowAnswer() - displayCardAnswer() - } /** * Changes which were received when the viewer was in the background @@ -335,21 +336,22 @@ abstract class AbstractFlashcardViewer : @VisibleForTesting internal var refreshRequired: ViewerRefresh? = null - private val editCurrentCardLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - FlashCardViewerResultCallback { result, reloadRequired -> - if (result.resultCode == RESULT_OK) { - Timber.i("AbstractFlashcardViewer:: card edited...") - 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) { + Timber.i("AbstractFlashcardViewer:: card edited...") + 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) { @@ -377,12 +379,18 @@ abstract class AbstractFlashcardViewer : } // Event handler for eases (answer buttons) - inner class SelectEaseHandler : View.OnClickListener, OnTouchListener { + inner class SelectEaseHandler : + View.OnClickListener, + OnTouchListener { private var prevCard: Card? = null private var hasBeenTouched = false private var touchX = 0f private var touchY = 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 prevCard = currentCard @@ -455,20 +463,21 @@ abstract class AbstractFlashcardViewer : @get:VisibleForTesting protected open val elapsedRealTime: Long get() = SystemClock.elapsedRealtime() - private val gestureListener = OnTouchListener { _, event -> - if (gestureDetector!!.onTouchEvent(event)) { - return@OnTouchListener true - } - if (!gestureDetectorImpl.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 gestureListener = + OnTouchListener { _, event -> + if (gestureDetector!!.onTouchEvent(event)) { + return@OnTouchListener true + } + if (!gestureDetectorImpl.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") @@ -504,11 +513,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(this@withCol, reload = false, browser = false) + currentCard = + withCol { + sched.card?.apply { + renderOutput(this@withCol, reload = false, browser = false) + } } - } } internal suspend fun updateCardAndRedraw() { @@ -564,9 +574,7 @@ abstract class AbstractFlashcardViewer : TtsVoicesFieldFilter.ensureApplied() } - protected open fun getContentViewAttr(fullscreenMode: FullScreenMode): Int { - return R.layout.reviewer - } + protected open fun getContentViewAttr(fullscreenMode: FullScreenMode): Int = R.layout.reviewer @get:VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) val isFullscreen: Boolean @@ -676,13 +684,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 } @@ -703,7 +714,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 } @@ -712,7 +726,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 (doubleScrolling) { @@ -730,13 +747,9 @@ abstract class AbstractFlashcardViewer : return false } - protected open fun answerFieldIsFocused(): Boolean { - return answerField != null && answerField!!.isFocused - } + protected open fun answerFieldIsFocused(): Boolean = answerField != null && answerField!!.isFocused - protected fun clipboardHasText(): Boolean { - return !getText(clipboard).isNullOrEmpty() - } + protected fun clipboardHasText(): Boolean = !getText(clipboard).isNullOrEmpty() /** * Returns the text stored in the clipboard or the empty string if the clipboard is empty or contains something that @@ -764,9 +777,7 @@ abstract class AbstractFlashcardViewer : * Currently, this is used for note edits - in a reviewing context, this should show the next card. * In a previewing context, the card should not change. */ - open fun canAccessScheduler(): Boolean { - return false - } + open fun canAccessScheduler(): Boolean = false protected open fun onEditedNoteChanged() {} @@ -791,15 +802,12 @@ abstract class AbstractFlashcardViewer : } /** Whether the callback to onCollectionLoaded has loaded card content */ - private fun hasLoadedCardContent(): Boolean { - return cardContent != null - } + private fun hasLoadedCardContent(): Boolean = cardContent != null - open fun undo(): Job { - return launchCatchingTask { + open fun undo(): Job = + launchCatchingTask { undoAndShowSnackbar(duration = Reviewer.ACTION_SNACKBAR_TIME) } - } private fun finishNoStorageAvailable() { this@AbstractFlashcardViewer.setResult(DeckPicker.RESULT_MEDIA_EJECTED) @@ -821,15 +829,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.stripHTMLAndSpecialFields(currentCard!!.question(getColUnsafe, true)).trim() - ) + text = + resources.getString( + R.string.delete_note_message, + Utils.stripHTMLAndSpecialFields(currentCard!!.question(getColUnsafe, true)).trim(), + ), ) positiveButton(R.string.dialog_positive_delete) { Timber.i( "AbstractFlashcardViewer:: OK button pressed to delete note %d", - currentCard!!.nid + currentCard!!.nid, ) launchCatchingTask { cardMediaPlayer.stopSounds() } deleteNoteWithoutConfirmation() @@ -842,41 +851,44 @@ 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(ease: Ease) = preventSimultaneousExecutions(ANSWER_CARD) { - launchCatchingTask { - if (inAnswer) { - return@launchCatchingTask - } - isSelecting = false - if (previousAnswerIndicator == null) { - // workaround for a broken ReviewerKeyboardInputTest - return@launchCatchingTask - } - // Temporarily sets the answer indicator dots appearing below the toolbar - previousAnswerIndicator?.displayAnswerIndicator(ease) - cardMediaPlayer.stopSounds() - currentEase = ease + open fun answerCard(ease: Ease) = + preventSimultaneousExecutions(ANSWER_CARD) { + launchCatchingTask { + if (inAnswer) { + return@launchCatchingTask + } + isSelecting = false + if (previousAnswerIndicator == null) { + // workaround for a broken ReviewerKeyboardInputTest + return@launchCatchingTask + } + // Temporarily sets the answer indicator dots appearing below the toolbar + previousAnswerIndicator?.displayAnswerIndicator(ease) + cardMediaPlayer.stopSounds() + currentEase = ease - answerCardInner(ease) - updateCardAndRedraw() + answerCardInner(ease) + updateCardAndRedraw() + } } - } open suspend fun answerCardInner(ease: Ease) { // Legacy tests assume they can call answerCard() even outside of Reviewer @@ -898,30 +910,34 @@ abstract class AbstractFlashcardViewer : // Initialize swipe gestureDetector = GestureDetector(this, gestureDetectorImpl) easeButtonsLayout = findViewById(R.id.ease_buttons) - easeButton1 = EaseButton( - Ease.AGAIN, - findViewById(R.id.flashcard_layout_ease1), - findViewById(R.id.ease1), - findViewById(R.id.nextTime1) - ).apply { setListeners(easeHandler) } - easeButton2 = EaseButton( - Ease.HARD, - findViewById(R.id.flashcard_layout_ease2), - findViewById(R.id.ease2), - findViewById(R.id.nextTime2) - ).apply { setListeners(easeHandler) } - easeButton3 = EaseButton( - Ease.GOOD, - findViewById(R.id.flashcard_layout_ease3), - findViewById(R.id.ease3), - findViewById(R.id.nextTime3) - ).apply { setListeners(easeHandler) } - easeButton4 = EaseButton( - Ease.EASY, - findViewById(R.id.flashcard_layout_ease4), - findViewById(R.id.ease4), - findViewById(R.id.nextTime4) - ).apply { setListeners(easeHandler) } + easeButton1 = + EaseButton( + Ease.AGAIN, + findViewById(R.id.flashcard_layout_ease1), + findViewById(R.id.ease1), + findViewById(R.id.nextTime1), + ).apply { setListeners(easeHandler) } + easeButton2 = + EaseButton( + Ease.HARD, + findViewById(R.id.flashcard_layout_ease2), + findViewById(R.id.ease2), + findViewById(R.id.nextTime2), + ).apply { setListeners(easeHandler) } + easeButton3 = + EaseButton( + Ease.GOOD, + findViewById(R.id.flashcard_layout_ease3), + findViewById(R.id.ease3), + findViewById(R.id.nextTime3), + ).apply { setListeners(easeHandler) } + easeButton4 = + EaseButton( + Ease.EASY, + findViewById(R.id.flashcard_layout_ease4), + findViewById(R.id.ease4), + findViewById(R.id.nextTime4), + ).apply { setListeners(easeHandler) } if (!showNextReviewTime) { easeButton1!!.hideNextReviewTime() easeButton2!!.hideNextReviewTime() @@ -974,10 +990,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", + ) this.answerButtonsPosition = answerButtonsPosition val answerArea = findViewById(R.id.bottom_area_layout) val answerAreaParams = answerArea.layoutParams as RelativeLayout.LayoutParams @@ -997,7 +1014,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) @@ -1026,32 +1044,33 @@ abstract class AbstractFlashcardViewer : protected open fun createWebView(): WebView { val resourceHandler = ViewerResourceHandler(this) - val webView: WebView = MyWebView(this).apply { - scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY - with(settings) { - displayZoomControls = false - builtInZoomControls = true - setSupportZoom(true) - loadWithOverviewMode = true - javaScriptEnabled = true - allowFileAccess = true - // enable dom storage so that sessionStorage & localStorage can be used in webview - domStorageEnabled = true - } - webChromeClient = AnkiDroidWebChromeClient() - isFocusableInTouchMode = typeAnswer!!.useInputTag - isScrollbarFadingEnabled = true - // Set transparent color to prevent flashing white when night mode enabled - setBackgroundColor(Color.argb(1, 0, 0, 0)) - CardViewerWebClient(resourceHandler, this@AbstractFlashcardViewer).apply { - webViewClient = this - this@AbstractFlashcardViewer.webViewClient = this + val webView: WebView = + MyWebView(this).apply { + scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY + with(settings) { + displayZoomControls = false + builtInZoomControls = true + setSupportZoom(true) + loadWithOverviewMode = true + javaScriptEnabled = true + allowFileAccess = true + // enable dom storage so that sessionStorage & localStorage can be used in webview + domStorageEnabled = true + } + webChromeClient = AnkiDroidWebChromeClient() + isFocusableInTouchMode = typeAnswer!!.useInputTag + isScrollbarFadingEnabled = true + // Set transparent color to prevent flashing white when night mode enabled + setBackgroundColor(Color.argb(1, 0, 0, 0)) + CardViewerWebClient(resourceHandler, this@AbstractFlashcardViewer).apply { + webViewClient = this + this@AbstractFlashcardViewer.webViewClient = this + } } - } Timber.d( "Focusable = %s, Focusable in touch mode = %s", webView.isFocusable, - webView.isFocusableInTouchMode + webView.isFocusableInTouchMode, ) // enable third party cookies so that cookies can be used in webview @@ -1080,10 +1099,14 @@ 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() @@ -1102,9 +1125,7 @@ abstract class AbstractFlashcardViewer : } } - protected fun shouldShowNextReviewTime(): Boolean { - return showNextReviewTime - } + protected fun shouldShowNextReviewTime(): Boolean = showNextReviewTime protected open fun displayAnswerBottomBar() { flipCardLayout!!.isClickable = false @@ -1134,7 +1155,10 @@ abstract class AbstractFlashcardViewer : after.run() } else { flipCardLayout!!.alpha = 1f - flipCardLayout!!.animate().alpha(0f).setDuration(shortAnimDuration.toLong()) + flipCardLayout!! + .animate() + .alpha(0f) + .setDuration(shortAnimDuration.toLong()) .withEndAction(after) } } @@ -1148,7 +1172,10 @@ abstract class AbstractFlashcardViewer : after.run() } else { flipCardLayout?.alpha = 0f - flipCardLayout?.animate()?.alpha(1f)?.setDuration(shortAnimDuration.toLong()) + flipCardLayout + ?.animate() + ?.alpha(1f) + ?.setDuration(shortAnimDuration.toLong()) ?.withEndAction(after) } focusAnswerCompletionField() @@ -1166,15 +1193,16 @@ abstract class AbstractFlashcardViewer : * Focuses the appropriate field for an answer * And allows keyboard shortcuts to go to the default handlers. */ - private fun focusAnswerCompletionField() = runOnUiThread { - // This does not handle mUseInputTag (the WebView contains an input field with a typable answer). - // In this case, the user can use touch to focus the field if necessary. - if (typeAnswer?.autoFocusEditText() == true) { - answerField?.focusWithKeyboard() - } else { - flipCardLayout?.requestFocus() + private fun focusAnswerCompletionField() = + runOnUiThread { + // This does not handle mUseInputTag (the WebView contains an input field with a typable answer). + // In this case, the user can use touch to focus the field if necessary. + if (typeAnswer?.autoFocusEditText() == true) { + answerField?.focusWithKeyboard() + } else { + flipCardLayout?.requestFocus() + } } - } protected open fun switchTopBarVisibility(visible: Int) { previousAnswerIndicator!!.setVisibility(visible) @@ -1221,9 +1249,10 @@ abstract class AbstractFlashcardViewer : if (gesturesEnabled) { gestureProcessor.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) @@ -1296,11 +1325,10 @@ abstract class AbstractFlashcardViewer : } } - private suspend fun automaticAnswerShouldWaitForAudio(): Boolean { - return withCol { + private suspend fun automaticAnswerShouldWaitForAudio(): Boolean = + withCol { decks.configDictForDeckId(currentCard!!.did).optBoolean("waitForAudio", true) } - } internal inner class ReadTextListener : ReadText.ReadTextListener { override fun onDone(playedSide: CardSide?) { @@ -1335,7 +1363,7 @@ abstract class AbstractFlashcardViewer : // If Card-based TTS is enabled, we "automatic display" after the TTS has finished as we don't know the duration Timber.i( "AbstractFlashcardViewer:: Question successfully shown for card id %d", - currentCard!!.id + currentCard!!.id, ) } @@ -1388,37 +1416,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,7 +1544,7 @@ abstract class AbstractFlashcardViewer : getColUnsafe, this, currentCard!!, - if (displayAnswer) CardSide.ANSWER else CardSide.QUESTION + if (displayAnswer) CardSide.ANSWER else CardSide.QUESTION, ) } } @@ -1529,7 +1562,10 @@ abstract class AbstractFlashcardViewer : } } - private fun loadContentIntoCard(card: WebView?, content: String) { + private fun loadContentIntoCard( + card: WebView?, + content: String, + ) { if (card != null) { card.settings.mediaPlaybackRequiresUserGesture = !cardMediaPlayer.config.autoplay card.loadDataWithBaseURL( @@ -1537,7 +1573,7 @@ abstract class AbstractFlashcardViewer : content, "text/html", null, - null + null, ) } } @@ -1587,11 +1623,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) cardMediaPlayer.stopSounds() @@ -1603,18 +1640,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)) + } } - } cardMediaPlayer.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) { @@ -1754,16 +1795,15 @@ abstract class AbstractFlashcardViewer : ViewerCommand.USER_ACTION_6, ViewerCommand.USER_ACTION_7, ViewerCommand.USER_ACTION_8, - ViewerCommand.USER_ACTION_9 -> { + ViewerCommand.USER_ACTION_9, + -> { Timber.w("Unknown command requested: %s", which) false } } } - fun executeCommand(which: ViewerCommand): Boolean { - return executeCommand(which, fromGesture = null) - } + fun executeCommand(which: ViewerCommand): Boolean = executeCommand(which, fromGesture = null) protected open fun replayVoice() { // intentionally blank @@ -1828,6 +1868,7 @@ abstract class AbstractFlashcardViewer : // ---------------------------------------------------------------------------- // INNER CLASSES // ---------------------------------------------------------------------------- + /** * Provides a hook for calling "alert" from javascript. Useful for debugging your javascript. */ @@ -1836,7 +1877,7 @@ abstract class AbstractFlashcardViewer : view: WebView, url: String, message: String, - result: JsResult + result: JsResult, ): Boolean { Timber.i("AbstractFlashcardViewer:: onJsAlert: %s", message) result.confirm() @@ -1860,15 +1901,15 @@ abstract class AbstractFlashcardViewer : // to avoid destroying the View if the device is rotated override fun onShowCustomView( paramView: View, - paramCustomViewCallback: CustomViewCallback? + paramCustomViewCallback: CustomViewCallback?, ) { customView = paramView (window.decorView as FrameLayout).addView( customView, FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ) + FrameLayout.LayoutParams.MATCH_PARENT, + ), ) // hide system bars with(WindowInsetsControllerCompat(window, window.decorView)) { @@ -1929,7 +1970,7 @@ abstract class AbstractFlashcardViewer : hideEaseButtons() Timber.i( "AbstractFlashcardViewer:: Question successfully shown for card id %d", - currentCard!!.id + currentCard!!.id, ) } else { displayCardAnswer() @@ -1937,13 +1978,15 @@ abstract class AbstractFlashcardViewer : } /** Fixing bug 720: focus, thanks to pablomouzo on android issue 7189 */ - internal inner class MyWebView(context: Context?) : WebView(context!!) { + internal inner class MyWebView( + context: Context?, + ) : WebView(context!!) { override fun loadDataWithBaseURL( baseUrl: String?, data: String, mimeType: String?, encoding: String?, - historyUrl: String? + historyUrl: String?, ) { if (!this@AbstractFlashcardViewer.isDestroyed) { super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl) @@ -1952,7 +1995,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)) { isXScrolling = true @@ -1977,7 +2025,7 @@ abstract class AbstractFlashcardViewer : scrollX: Int, scrollY: Int, clampedX: Boolean, - clampedY: Boolean + clampedY: Boolean, ) { if (clampedX) { val scrollParent = findScrollParent(this) @@ -2006,7 +2054,7 @@ abstract class AbstractFlashcardViewer : e1: MotionEvent?, e2: MotionEvent, velocityX: Float, - velocityY: Float + velocityY: Float, ): Boolean { Timber.d("onFling") @@ -2031,7 +2079,7 @@ abstract class AbstractFlashcardViewer : velocityY, isSelecting, isXScrolling, - isYScrolling + isYScrolling, ) } catch (e: Exception) { Timber.e(e, "onFling Exception") @@ -2054,9 +2102,7 @@ abstract class AbstractFlashcardViewer : return true } - override fun onSingleTapUp(e: MotionEvent): Boolean { - return false - } + override fun onSingleTapUp(e: MotionEvent): Boolean = false override fun onSingleTapConfirmed(e: MotionEvent): Boolean { // Go back to immersive mode if the user had temporarily exited it (and ignore the tap gesture) @@ -2086,9 +2132,7 @@ abstract class AbstractFlashcardViewer : // intentionally blank } - open fun eventCanBeSentToWebView(event: MotionEvent): Boolean { - return true - } + open fun eventCanBeSentToWebView(event: MotionEvent): Boolean = true open fun startShakeDetector() { // intentionally blank @@ -2099,16 +2143,15 @@ abstract class AbstractFlashcardViewer : } } - protected open fun onSingleTap(): Boolean { - return false - } + protected open fun onSingleTap(): Boolean = false protected open fun onFling() {} /** #6141 - blocks clicking links from executing "touch" gestures. * COULD_BE_BETTER: Make base class static and move this out of the CardViewer */ internal inner class LinkDetectingGestureDetector : - MyGestureDetector(), ShakeDetector.Listener { + MyGestureDetector(), + ShakeDetector.Listener { private var shakeDetector: ShakeDetector? = null init { @@ -2119,9 +2162,10 @@ abstract class AbstractFlashcardViewer : Timber.d("Initializing shake detector") if (gestureProcessor.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) + } } } @@ -2195,12 +2239,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 @@ -2219,7 +2264,7 @@ abstract class AbstractFlashcardViewer : return ( type == HitTestResult.SRC_ANCHOR_TYPE || type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE - ) + ) } } @@ -2231,9 +2276,7 @@ abstract class AbstractFlashcardViewer : } } - protected open fun shouldDisplayMark(): Boolean { - return isMarked(getColUnsafe, currentCard!!.note(getColUnsafe)) - } + protected open fun shouldDisplayMark(): Boolean = isMarked(getColUnsafe, currentCard!!.note(getColUnsafe)) val writeLock: Lock get() = cardLock.writeLock() @@ -2278,7 +2321,8 @@ abstract class AbstractFlashcardViewer : ANSWER_ORDINAL_1, ANSWER_ORDINAL_2, ANSWER_ORDINAL_3, - ANSWER_ORDINAL_4; + ANSWER_ORDINAL_4, + ; companion object { fun String.toSignal(): Signal { @@ -2300,10 +2344,12 @@ abstract class AbstractFlashcardViewer : } } } + inner class CardViewerWebClient internal constructor( private val resourceHandler: ViewerResourceHandler, - private val onPageFinishedCallback: OnPageFinishedCallback? = null - ) : WebViewClient(), JavascriptEvaluator { + private val onPageFinishedCallback: OnPageFinishedCallback? = null, + ) : WebViewClient(), + JavascriptEvaluator { private var pageFinishedFired = true private val pageRenderStopwatch = Stopwatch.init("page render") @@ -2313,20 +2359,27 @@ abstract class AbstractFlashcardViewer : return filterUrl(url) } - 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) } - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + override fun onPageStarted( + view: WebView?, + url: String?, + favicon: Bitmap?, + ) { pageRenderStopwatch.reset() pageFinishedFired = false } override fun shouldInterceptRequest( view: WebView, - request: WebResourceRequest + request: WebResourceRequest, ): WebResourceResponse? { resourceHandler.shouldInterceptRequest(request)?.let { return it } return null @@ -2335,12 +2388,12 @@ abstract class AbstractFlashcardViewer : override fun onReceivedError( view: WebView, request: WebResourceRequest, - error: WebResourceError + error: WebResourceError, ) { super.onReceivedError(view, request, error) mediaErrorHandler.processFailure(request) { filename: String -> displayCouldNotFindMediaSnackbar( - filename + filename, ) } } @@ -2348,12 +2401,12 @@ abstract class AbstractFlashcardViewer : override fun onReceivedHttpError( view: WebView, request: WebResourceRequest, - errorResponse: WebResourceResponse + errorResponse: WebResourceResponse, ) { super.onReceivedHttpError(view, request, errorResponse) mediaErrorHandler.processFailure(request) { filename: String -> displayCouldNotFindMediaSnackbar( - filename + filename, ) } } @@ -2470,28 +2523,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 @@ -2527,17 +2581,21 @@ abstract class AbstractFlashcardViewer : */ @NeedsTest("14221: 'playsound' should play the sound from the start") private suspend fun controlSound(url: String) { - val avTag = when (val tag = currentCard?.let { getAvTag(it, url) }) { - is SoundOrVideoTag -> tag - is TTSTag -> tag - // not currently supported - null -> return - } + val avTag = + when (val tag = currentCard?.let { getAvTag(it, url) }) { + is SoundOrVideoTag -> tag + is TTSTag -> tag + // not currently supported + null -> return + } cardMediaPlayer.playOneSound(avTag) } // 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 } @@ -2555,9 +2613,10 @@ abstract class AbstractFlashcardViewer : } @TargetApi(Build.VERSION_CODES.O) - override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean { - return onRenderProcessGoneDelegate.onRenderProcessGone(view, detail) - } + override fun onRenderProcessGone( + view: WebView, + detail: RenderProcessGoneDetail, + ): Boolean = onRenderProcessGoneDelegate.onRenderProcessGone(view, detail) override fun eval(js: String) { // WARNING: it is not guaranteed that card.js has loaded at this point @@ -2576,7 +2635,7 @@ abstract class AbstractFlashcardViewer : showThemedToast( this@AbstractFlashcardViewer, getString(R.string.card_viewer_url_decode_error), - true + true, ) } return "" @@ -2604,15 +2663,17 @@ abstract class AbstractFlashcardViewer : internal fun showTagsDialog() { val tags = ArrayList(getColUnsafe.tags.all()) val selTags = ArrayList(currentCard!!.note(getColUnsafe).tags) - val dialog = tagsDialogFactory!!.newTagsDialog() - .withArguments(this, TagsDialog.DialogType.EDIT_TAGS, selTags, tags) + val dialog = + tagsDialogFactory!! + .newTagsDialog() + .withArguments(this, TagsDialog.DialogType.EDIT_TAGS, selTags, tags) showDialogFragment(dialog) } override fun onSelectedTags( selectedTags: List, indeterminateTags: List, - stateFilter: CardStateFilter + stateFilter: CardStateFilter, ) { launchCatchingTask { val note = withCol { currentCard!!.note(this@withCol) } @@ -2625,27 +2686,30 @@ abstract class AbstractFlashcardViewer : } } - override fun opExecuted(changes: OpChanges, handler: Any?) { + override fun opExecuted( + changes: OpChanges, + handler: Any?, + ) { if (handler === this) return refreshRequired = ViewerRefresh.updateState(refreshRequired, changes) refreshIfRequired() } - open fun getCardDataForJsApi(): AnkiDroidJsAPI.CardDataForJsApi { - return AnkiDroidJsAPI.CardDataForJsApi() - } + open fun getCardDataForJsApi(): AnkiDroidJsAPI.CardDataForJsApi = AnkiDroidJsAPI.CardDataForJsApi() - override suspend fun handlePostRequest(uri: String, bytes: ByteArray): ByteArray { - return if (uri.startsWith(AnkiServer.ANKIDROID_JS_PREFIX)) { + override suspend fun handlePostRequest( + uri: String, + bytes: ByteArray, + ): ByteArray = + if (uri.startsWith(AnkiServer.ANKIDROID_JS_PREFIX)) { jsApi.handleJsApiRequest( uri.substring(AnkiServer.ANKIDROID_JS_PREFIX.length), bytes, - returnDefaultValues = true + returnDefaultValues = true, ) } else { throw IllegalArgumentException("unhandled request: $uri") } - } companion object { /** @@ -2680,15 +2744,14 @@ abstract class AbstractFlashcardViewer : * @return if [gesture] is a swipe, a transition to the same direction of the swipe * else return [ActivityTransitionAnimation.Direction.FADE] */ - fun getAnimationTransitionFromGesture(gesture: Gesture?): ActivityTransitionAnimation.Direction { - return when (gesture) { + fun getAnimationTransitionFromGesture(gesture: Gesture?): ActivityTransitionAnimation.Direction = + when (gesture) { Gesture.SWIPE_UP -> ActivityTransitionAnimation.Direction.UP Gesture.SWIPE_DOWN -> ActivityTransitionAnimation.Direction.DOWN Gesture.SWIPE_RIGHT -> ActivityTransitionAnimation.Direction.RIGHT Gesture.SWIPE_LEFT -> ActivityTransitionAnimation.Direction.LEFT else -> ActivityTransitionAnimation.Direction.FADE } - } fun Gesture?.toAnimationTransition() = getAnimationTransitionFromGesture(this) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt index cdf4af38142d..4257a6cf93a6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ActionProviderCompat.kt @@ -25,8 +25,9 @@ import androidx.core.view.ActionProvider * * @see ActionProvider */ -abstract class ActionProviderCompat(context: Context) : ActionProvider(context) { - +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 fc7b53fd15e1..850a26f08df7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AndroidTtsPlayer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AndroidTtsPlayer.kt @@ -35,9 +35,10 @@ import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber import kotlin.coroutines.resume -class AndroidTtsPlayer(private val context: Context, private val voices: List) : - TtsPlayer() { - +class AndroidTtsPlayer( + private val context: Context, + private val voices: List, +) : TtsPlayer() { private lateinit var scope: CoroutineScope // this can be null in the case that TTS failed to load @@ -53,35 +54,42 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List { - return this.voices - } + override fun getAvailableVoices(): List = this.voices override suspend fun play(tag: TTSTag): TtsCompletionStatus { val match = voiceForTag(tag) @@ -101,25 +109,30 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List - val tts = tts?.also { - it.voice = voice.voice - tag.speed?.let { speed -> - if (it.setSpeechRate(speed) == ERROR) { - return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.SpeechRateFailed) + val tts = + tts?.also { + it.voice = voice.voice + tag.speed?.let { speed -> + if (it.setSpeechRate(speed) == ERROR) { + return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.SpeechRateFailed) + } } - } - // if it's already playing: stop it - it.stopPlaying() - } ?: return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.InitFailed) + // if it's already playing: stop it + it.stopPlaying() + } ?: return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.InitFailed) Timber.d("tts text '%s' to be played for locale (%s)", tag.fieldText, tag.lang) continuation.ensureActive() - val utteranceId = tag.fieldText.hashCode().toString().apply { - currentUtterance = this - cancelledUtterances.remove(this) - } + val utteranceId = + tag.fieldText.hashCode().toString().apply { + currentUtterance = this + cancelledUtterances.remove(this) + } tts.speak(tag.fieldText, TextToSpeech.QUEUE_FLUSH, bundleFlyweight, utteranceId) continuation.invokeOnCancellation { @@ -134,7 +147,7 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List= Build.VERSION_CODES.O_MR1) { @@ -146,7 +150,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity 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() @@ -185,9 +189,10 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity * By default it handles [SdCardReceiver.MEDIA_EJECT], and shows/dismisses dialogs when an SD * card is ejected/remounted (collection is saved beforehand by [SdCardReceiver]) */ - protected open val broadcastsActions = mapOf( - SdCardReceiver.MEDIA_EJECT to { onSdCardNotMounted() } - ) + protected open val broadcastsActions = + mapOf( + SdCardReceiver.MEDIA_EJECT to { onSdCardNotMounted() }, + ) /** * Register a broadcast receiver, associating an intent to an action as in [broadcastsActions]. @@ -198,15 +203,19 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity // Receiver already registered return } - broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - broadcastsActions[intent.action]?.invoke() + broadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + broadcastsActions[intent.action]?.invoke() + } + }.also { + val iFilter = IntentFilter() + broadcastsActions.keys.map(iFilter::addAction) + registerReceiverCompat(it, iFilter, ContextCompat.RECEIVER_EXPORTED) } - }.also { - val iFilter = IntentFilter() - broadcastsActions.keys.map(iFilter::addAction) - registerReceiverCompat(it, iFilter, ContextCompat.RECEIVER_EXPORTED) - } } protected fun onSdCardNotMounted() { @@ -219,9 +228,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity val getColUnsafe: Collection get() = CollectionManager.getColUnsafe() - fun colIsOpenUnsafe(): Boolean { - return CollectionManager.isOpenUnsafe() - } + fun colIsOpenUnsafe(): Boolean = CollectionManager.isOpenUnsafe() /** * Whether animations should not be displayed @@ -242,9 +249,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity * * @see .animationDisabled */ - fun animationEnabled(): Boolean { - return !animationDisabled() - } + fun animationEnabled(): Boolean = !animationDisabled() override fun setContentView(view: View?) { if (animationDisabled()) { @@ -253,14 +258,20 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity 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() } @@ -279,7 +290,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity fun startActivityWithAnimation( intent: Intent, - animation: Direction + animation: Direction, ) { enableIntentAnimation(intent) super.startActivity(intent) @@ -289,12 +300,12 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity 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) @@ -317,7 +328,10 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity view.clearAnimation() } - protected fun enableViewAnimation(view: View, animation: Animation?) { + protected fun enableViewAnimation( + view: View, + animation: Animation?, + ) { if (animationDisabled()) { disableViewAnimation(view) } else { @@ -359,7 +373,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity // 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") @@ -416,22 +430,24 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity } 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 = CustomTabsIntentBuilder(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) + val colorSchemeParams = + CustomTabColorSchemeParams + .Builder() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navBarColor) + .build() + val builder = + CustomTabsIntentBuilder(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) val customTabsIntent = builder.build() CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent) CustomTabActivityHelper.openCustomTab(this, customTabsIntent, url, CustomTabsFallback()) @@ -441,18 +457,21 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity 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 + } /** * Calls [.showAsyncDialogFragment] internally, using the channel @@ -474,7 +493,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity */ fun showAsyncDialogFragment( newFragment: AsyncDialogFragment, - channel: Channel + channel: Channel, ) { try { showDialogFragment(newFragment) @@ -500,7 +519,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity open fun showSimpleMessageDialog( message: String, title: String = "", - reload: Boolean = false + reload: Boolean = false, ) { val newFragment: AsyncDialogFragment = SimpleMessageDialog.newInstance(title, message, reload) @@ -510,31 +529,34 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity 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(getString(R.string.pref_notifications_minimum_cards_due_key), "0")!! - .toInt() <= PENDING_NOTIFICATIONS_ONLY + if (prefs + .getString(getString(R.string.pref_notifications_minimum_cards_due_key), "0")!! + .toInt() <= 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)) @@ -545,13 +567,14 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity // 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. @@ -560,7 +583,10 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity } // Show dialogs to deal with database loading issues etc - open fun showDatabaseErrorDialog(errorDialogType: DatabaseErrorDialogType, exceptionData: CustomExceptionData? = null) { + open fun showDatabaseErrorDialog( + errorDialogType: DatabaseErrorDialogType, + exceptionData: CustomExceptionData? = null, + ) { val newFragment: AsyncDialogFragment = DatabaseErrorDialog.newInstance(errorDialogType, exceptionData) showAsyncDialogFragment(newFragment) } @@ -571,9 +597,10 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity * @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!! } @@ -585,9 +612,10 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity * @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!! } @@ -595,12 +623,14 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity protected fun showedActivityFailedScreen(savedInstanceState: Bundle?) = showedActivityFailedScreen( savedInstanceState = savedInstanceState, - activitySuperOnCreate = { state -> super.onCreate(state) } + activitySuperOnCreate = { state -> super.onCreate(state) }, ) /** @see Window.setNavigationBarColor */ @Suppress("deprecation", "API35 properly handle edge-to-edge") - fun setNavigationBarColor(@AttrRes attr: Int) { + fun setNavigationBarColor( + @AttrRes attr: Int, + ) { window.navigationBarColor = Themes.getColorFromAttr(this, attr) } @@ -614,7 +644,7 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity override fun onProvideKeyboardShortcuts( data: MutableList, menu: Menu?, - deviceId: Int + deviceId: Int, ) { val shortcutGroups = getShortcuts() data.addAll(shortcutGroups) @@ -637,18 +667,22 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity * Get current activity keyboard shortcuts */ fun getShortcuts(): List { - val generalShortcutGroup = ShortcutGroup( - listOf( - shortcut("Alt+K", R.string.show_keyboard_shortcuts_dialog), - shortcut("Ctrl+Z", R.string.undo) - ), - R.string.pref_cat_general - ).toShortcutGroup(this) + val generalShortcutGroup = + ShortcutGroup( + listOf( + shortcut("Alt+K", R.string.show_keyboard_shortcuts_dialog), + shortcut("Ctrl+Z", R.string.undo), + ), + R.string.pref_cat_general, + ).toShortcutGroup(this) return listOfNotNull(shortcuts?.toShortcutGroup(this), generalShortcutGroup) } - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { if (event.isAltPressed && keyCode == KeyEvent.KEYCODE_K) { showKeyboardShortcutsDialog() return true @@ -687,7 +721,6 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity get(): ShortcutGroup? = null companion object { - /** Extra key to set the finish animation of an activity */ const val FINISH_ANIMATION_EXTRA = "finishAnimation" @@ -695,10 +728,9 @@ open class AnkiActivity : AppCompatActivity, ShortcutGroupProvider, AnkiActivity } } -fun Fragment.requireAnkiActivity(): AnkiActivity { - return requireActivity() as? AnkiActivity? +fun Fragment.requireAnkiActivity(): AnkiActivity = + requireActivity() as? AnkiActivity? ?: throw java.lang.IllegalStateException("Fragment $this not attached to an AnkiActivity.") -} interface AnkiActivityProvider { val ankiActivity: AnkiActivity diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt index 91ec54705ce3..c30f124b3416 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt @@ -76,7 +76,10 @@ import java.util.Locale */ @KotlinCleanup("lots to do") @KotlinCleanup("IDE Lint") -open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.Subscriber { +open class AnkiDroidApp : + Application(), + Configuration.Provider, + ChangeManager.Subscriber { /** An exception if the WebView subsystem fails to load */ private var webViewError: Throwable? = null private val notifications = MutableLiveData() @@ -171,12 +174,12 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S 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) @@ -214,41 +217,49 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S // listen for day rollover: time + timezone changes DayRolloverHandler.listenForRolloverEvents(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() @@ -267,7 +278,7 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S "android:%s:%s:%s", BuildConfig.VERSION_NAME, Build.VERSION.RELEASE, - model + model, ) } @@ -276,8 +287,8 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S } @Suppress("deprecation") // 7109: setAcceptFileSchemeCookies - protected fun acceptFileSchemeCookies(): Boolean { - return try { + protected fun acceptFileSchemeCookies(): Boolean = + try { CookieManager.setAcceptFileSchemeCookies(true) true } catch (e: Throwable) { @@ -289,7 +300,6 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S Timber.e(e, "setAcceptFileSchemeCookies") false } - } /** * Callback method invoked when operations that affect the app state are executed. @@ -299,7 +309,10 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S * @param changes The set of changes that occurred. * @param handler An optional handler that can be used for custom processing (unused here). */ - override fun opExecuted(changes: OpChanges, handler: Any?) { + override fun opExecuted( + changes: OpChanges, + handler: Any?, + ) { Timber.d("ChangeSubscriber - opExecuted called with changes: $changes") if (changes.studyQueues) { DeckPickerWidget.updateDeckPickerWidgets(this) @@ -310,7 +323,6 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S } companion object { - /** * [CoroutineScope] tied to the [Application], allowing executing of tasks which should * execute as long as the app is running @@ -334,6 +346,7 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S val sharedPreferencesProvider get() = SharedPreferencesProvider { sharedPrefs() } /** Running under instrumentation. a "/androidTest" directory will be created which contains a test collection */ + @Suppress("ktlint:standard:property-naming") var INSTRUMENTATION_TESTING = false const val XML_CUSTOM_NAMESPACE = "http://arbitrary.app.namespace/com.ichi2.anki" const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android" @@ -451,9 +464,7 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S else -> appResources.getString(R.string.link_manual) } - fun webViewFailedToLoad(): Boolean { - return instance.webViewError != null - } + fun webViewFailedToLoad(): Boolean = instance.webViewError != null val webViewErrorMessage: String? get() { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index a4e9159c7bc3..d86a761f5f4b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -61,7 +61,9 @@ typealias JvmFloat = Float typealias JvmLong = Long typealias JvmString = String -open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { +open class AnkiDroidJsAPI( + private val activity: AbstractFlashcardViewer, +) { private val currentCard: Card get() = activity.currentCard!! @@ -81,21 +83,25 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { // Speech to Text private val speechRecognizer = JavaScriptSTT(context) - open fun convertToByteArray(apiContract: ApiContract, boolean: Boolean): ByteArray { - return ApiResult.Boolean(apiContract.isValid, boolean).toString().toByteArray() - } + open fun convertToByteArray( + apiContract: ApiContract, + boolean: Boolean, + ): ByteArray = ApiResult.Boolean(apiContract.isValid, boolean).toString().toByteArray() - open fun convertToByteArray(apiContract: ApiContract, int: Int): ByteArray { - return ApiResult.Integer(apiContract.isValid, int).toString().toByteArray() - } + open fun convertToByteArray( + apiContract: ApiContract, + int: Int, + ): ByteArray = ApiResult.Integer(apiContract.isValid, int).toString().toByteArray() - open fun convertToByteArray(apiContract: ApiContract, long: Long): ByteArray { - return ApiResult.Long(apiContract.isValid, long).toString().toByteArray() - } + open fun convertToByteArray( + apiContract: ApiContract, + long: Long, + ): ByteArray = ApiResult.Long(apiContract.isValid, long).toString().toByteArray() - open fun convertToByteArray(apiContract: ApiContract, string: String): ByteArray { - return ApiResult.String(apiContract.isValid, string).toString().toByteArray() - } + open fun convertToByteArray( + apiContract: ApiContract, + string: String, + ): ByteArray = ApiResult.String(apiContract.isValid, string).toString().toByteArray() /** * The method parse json data and return api contract object @@ -127,7 +133,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) @@ -142,7 +151,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 { @@ -154,10 +166,10 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { val versionSupplied = Version.parse(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 @@ -189,7 +201,11 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * @param returnDefaultValues `true` if default values should be returned (if non-[Reviewer]) * @return */ - open suspend fun handleJsApiRequest(methodName: String, bytes: ByteArray, returnDefaultValues: Boolean = true) = withContext(Dispatchers.Main) { + open suspend fun handleJsApiRequest( + methodName: String, + bytes: ByteArray, + returnDefaultValues: Boolean = true, + ) = 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 @@ -218,7 +234,10 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } convertToByteArray(apiContract, activity.executeCommand(flagCommands[apiParams]!!)) } - "markCard" -> processAction({ activity.executeCommand(ViewerCommand.MARK) }, apiContract, ANKI_JS_ERROR_CODE_MARK_CARD, ::convertToByteArray) + "markCard" -> + processAction({ + activity.executeCommand(ViewerCommand.MARK) + }, apiContract, ANKI_JS_ERROR_CODE_MARK_CARD, ::convertToByteArray) "buryCard" -> processAction(activity::buryCard, apiContract, ANKI_JS_ERROR_CODE_BURY_CARD, ::convertToByteArray) "buryNote" -> processAction(activity::buryNote, apiContract, ANKI_JS_ERROR_CODE_BURT_NOTE, ::convertToByteArray) "suspendCard" -> processAction(activity::suspendCard, apiContract, ANKI_JS_ERROR_CODE_SUSPEND_CARD, ::convertToByteArray) @@ -279,10 +298,11 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } "ttsStop" -> convertToByteArray(apiContract, talker.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) } @@ -344,30 +364,33 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { val jsonObject = JSONObject(apiParams) val noteId = jsonObject.getLong("noteId") val tag = jsonObject.getString("tag") - val note = getColUnsafe.getNote(noteId).apply { - addTag(tag) - } + val note = + getColUnsafe.getNote(noteId).apply { + addTag(tag) + } getColUnsafe.updateNote(note) convertToByteArray(apiContract, true) } "sttSetLanguage" -> convertToByteArray(apiContract, speechRecognizer.setLanguage(apiParams)) "sttStart" -> { - val callback = object : JavaScriptSTT.SpeechRecognitionCallback { - override fun onResult(results: List) { - activity.lifecycleScope.launch { - val apiResult = ApiResult.success(Json.encodeToString(ListSerializer(String.serializer()), results)) - val jsonEncodedString = withContext(Dispatchers.Default) { JSONObject.quote(apiResult.toString()) } - activity.webView!!.evaluateJavascript("ankiSttResult($jsonEncodedString)", null) + val callback = + object : JavaScriptSTT.SpeechRecognitionCallback { + override fun onResult(results: List) { + activity.lifecycleScope.launch { + val apiResult = ApiResult.success(Json.encodeToString(ListSerializer(String.serializer()), results)) + val jsonEncodedString = withContext(Dispatchers.Default) { JSONObject.quote(apiResult.toString()) } + activity.webView!!.evaluateJavascript("ankiSttResult($jsonEncodedString)", null) + } } - } - override fun onError(errorMessage: String) { - activity.lifecycleScope.launch { - val apiResult = ApiResult.failure(errorMessage) - val jsonEncodedString = withContext(Dispatchers.Default) { JSONObject.quote(apiResult.toString()) } - activity.webView!!.evaluateJavascript("ankiSttResult($jsonEncodedString)", null) + + override fun onError(errorMessage: String) { + activity.lifecycleScope.launch { + val apiResult = ApiResult.failure(errorMessage) + val jsonEncodedString = withContext(Dispatchers.Default) { JSONObject.quote(apiResult.toString()) } + activity.webView!!.evaluateJavascript("ankiSttResult($jsonEncodedString)", null) + } } } - } speechRecognizer.setRecognitionCallback(callback) convertToByteArray(apiContract, speechRecognizer.start()) } @@ -383,7 +406,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) { @@ -392,44 +415,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.ANKI_JS_ERROR_CODE_SEARCH_CARD, apiContract.cardSuppliedDeveloperContact) - return@withContext convertToByteArray(apiContract, false) - } - val searchResult: MutableList = ArrayList() - for (card in cards.map { it.card }) { - val jsonObject = JSONObject() - val fieldsData = card.note(getColUnsafe).fields - val fieldsName = card.noteType(getColUnsafe).fieldsNames - - val noteId = card.nid - val cardId = 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) + 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.ANKI_JS_ERROR_CODE_SEARCH_CARD, apiContract.cardSuppliedDeveloperContact) + return@withContext convertToByteArray(apiContract, false) + } + val searchResult: MutableList = ArrayList() + for (card in cards.map { it.card }) { + val jsonObject = JSONObject() + val fieldsData = card.note(getColUnsafe).fields + val fieldsName = card.noteType(getColUnsafe).fieldsNames + + val noteId = card.nid + val cardId = 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) - 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: Int = -1 @@ -442,28 +467,49 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { var nextTime4 = "" } - sealed class ApiResult protected constructor(private val status: JvmBoolean) { - class Boolean(status: JvmBoolean, val value: JvmBoolean) : ApiResult(status) { + sealed class ApiResult protected constructor( + private val status: JvmBoolean, + ) { + class Boolean( + status: JvmBoolean, + val value: JvmBoolean, + ) : ApiResult(status) { override fun putValue(o: JSONObject) { o.put(VALUE_KEY, value) } } - class Integer(status: JvmBoolean, val value: JvmInt) : ApiResult(status) { + + class Integer( + status: JvmBoolean, + val value: JvmInt, + ) : ApiResult(status) { override fun putValue(o: JSONObject) { o.put(VALUE_KEY, value) } } - class Float(status: JvmBoolean, val value: JvmFloat) : ApiResult(status) { + + class Float( + status: JvmBoolean, + val value: JvmFloat, + ) : ApiResult(status) { override fun putValue(o: JSONObject) { o.put(VALUE_KEY, value) } } - class Long(status: JvmBoolean, val value: JvmLong) : ApiResult(status) { + + class Long( + status: JvmBoolean, + val value: JvmLong, + ) : ApiResult(status) { override fun putValue(o: JSONObject) { o.put(VALUE_KEY, value) } } - class String(status: JvmBoolean, val value: JvmString) : ApiResult(status) { + + class String( + status: JvmBoolean, + val value: JvmString, + ) : ApiResult(status) { override fun putValue(o: JSONObject) { o.put(VALUE_KEY, value) } @@ -471,19 +517,26 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { abstract fun putValue(o: JSONObject) - override fun toString() = JSONObject().apply { - put(SUCCESS_KEY, status) - putValue(this) - }.toString() + override fun toString() = + JSONObject() + .apply { + put(SUCCESS_KEY, status) + putValue(this) + }.toString() @Suppress("RemoveRedundantQualifierName") // we don't want `String(true, value)` companion object { fun success(value: JvmString) = ApiResult.String(true, value) + fun failure(value: JvmString) = ApiResult.String(false, value) } } - class ApiContract(val isValid: Boolean, val cardSuppliedDeveloperContact: String, val cardSuppliedData: String) + class ApiContract( + val isValid: Boolean, + val cardSuppliedDeveloperContact: String, + val cardSuppliedData: String, + ) companion object { /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt index 662e9aa285db..0ee0570346f9 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 CURRENT_JS_API_VERSION = "0.0.3" const val MINIMUM_JS_API_VERSION = "0.0.3" - 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/AnkiFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiFragment.kt index 869014424b85..ec95afc07e52 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiFragment.kt @@ -44,10 +44,10 @@ import timber.log.Timber * * @param layout Resource ID of the layout to be used for this fragment. */ -// TODO: Consider refactoring to create AnkiInterface to consolidate common implementations between AnkiFragment and AnkiActivity. -// This could help reduce code repetition and improve maintainability. -open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout), AnkiActivityProvider { - +open class AnkiFragment( + @LayoutRes layout: Int, +) : Fragment(layout), + AnkiActivityProvider { val getColUnsafe: Collection get() = CollectionManager.getColUnsafe() @@ -60,7 +60,10 @@ open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout), AnkiActivity // Open function: These can be overridden to react to specific parts of the lifecycle @Suppress("deprecation", "API35 properly handle edge-to-edge") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { requireActivity().window.statusBarColor = Themes.getColorFromAttr(requireContext(), R.attr.appBarColor) super.onViewCreated(view, savedInstanceState) } @@ -89,7 +92,9 @@ open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout), AnkiActivity protected suspend fun userAcceptsSchemaChange() = ankiActivity.userAcceptsSchemaChange() @Suppress("deprecation", "API35 properly handle edge-to-edge") - fun setNavigationBarColor(@AttrRes attr: Int) { + fun setNavigationBarColor( + @AttrRes attr: Int, + ) { requireActivity().window.navigationBarColor = Themes.getColorFromAttr(requireContext(), attr) } @@ -98,9 +103,9 @@ open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout), AnkiActivity * Finds a view in the fragment's layout by the specified ID. * */ - fun findViewById(@IdRes id: Int): T { - return requireView().findViewById(id) - } + fun findViewById( + @IdRes id: Int, + ): T = requireView().findViewById(id) /** * Unregisters a previously registered broadcast receiver. @@ -140,7 +145,9 @@ open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout), AnkiActivity * Sets the title of the toolbar. * */ - protected fun setTitle(@StringRes title: Int) { + protected fun setTitle( + @StringRes title: Int, + ) { mainToolbar.setTitle(title) } @@ -150,9 +157,8 @@ open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout), AnkiActivity */ protected suspend fun Fragment.withProgress( message: String = resources.getString(R.string.dialog_processing), - block: suspend () -> T - ): T = - requireActivity().withProgress(message, block) + block: suspend () -> T, + ): T = requireActivity().withProgress(message, block) /** * If storage permissions are not granted, shows a toast message and finishes the activity. diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt index 3ad8c4880b51..2be60921f315 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt @@ -38,7 +38,7 @@ fun Activity.importColpkg(colpkgPath: String) where Activity : AnkiAc if (progress.hasImporting()) { text = progress.importing } - } + }, ) { CollectionManager.importColpkg(colpkgPath) } @@ -53,7 +53,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/BackendExporting.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackendExporting.kt index f3a30779f24b..b400bf273353 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackendExporting.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackendExporting.kt @@ -26,7 +26,7 @@ fun AnkiActivity.exportApkgPackage( withScheduling: Boolean, withDeckConfigs: Boolean, withMedia: Boolean, - limit: ExportLimit + limit: ExportLimit, ) { launchCatchingTask { val onProgress: ProgressContext.() -> Unit = { @@ -44,7 +44,10 @@ fun AnkiActivity.exportApkgPackage( } } -suspend fun AnkiActivity.exportColpkg(colpkgPath: String, withMedia: Boolean) { +suspend fun AnkiActivity.exportColpkg( + colpkgPath: String, + withMedia: Boolean, +) { val onProgress: ProgressContext.() -> Unit = { if (progress.hasExporting()) { text = getString(R.string.export_preparation_in_progress) @@ -55,7 +58,10 @@ suspend fun AnkiActivity.exportColpkg(colpkgPath: String, withMedia: Boolean) { } } -fun AnkiActivity.exportCollectionPackage(exportPath: String, withMedia: Boolean) { +fun AnkiActivity.exportCollectionPackage( + exportPath: String, + withMedia: Boolean, +) { launchCatchingTask { exportColpkg(exportPath, withMedia) val factory = @@ -72,7 +78,7 @@ fun AnkiActivity.exportSelectedNotes( withDeck: Boolean, withNotetype: Boolean, withGuid: Boolean, - limit: ExportLimit + limit: ExportLimit, ) { launchCatchingTask { val onProgress: ProgressContext.() -> Unit = { @@ -89,7 +95,7 @@ fun AnkiActivity.exportSelectedNotes( withDeck, withNotetype, withGuid, - limit + limit, ) } } @@ -103,7 +109,7 @@ fun AnkiActivity.exportSelectedNotes( fun AnkiActivity.exportSelectedCards( exportPath: String, withHtml: Boolean, - limit: ExportLimit + limit: ExportLimit, ) { launchCatchingTask { val onProgress: ProgressContext.() -> Unit = { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt index e512e1ada1a6..56f19c940dd5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackendImporting.kt @@ -39,14 +39,13 @@ suspend fun importAnkiPackageUndoable(input: ByteArray): ByteArray { } } -suspend fun importCsvRaw(input: ByteArray): ByteArray { - return withContext(Dispatchers.Main) { +suspend fun importCsvRaw(input: ByteArray): ByteArray = + withContext(Dispatchers.Main) { val output = withCol { importCsvRaw(input) } val changes = OpChangesOnly.parseFrom(output) undoableOp { changes } output } -} /** * Css to hide the "Show" button from the final backend import page. As the user could import a lot @@ -56,7 +55,8 @@ suspend fun 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; }'; @@ -65,7 +65,7 @@ val hideShowButtonCss = """ document.head.appendChild(newStyle); } )() -""".trimIndent() + """.trimIndent() /** * Calls the native [CardBrowser] to display the results of the search query constructed from the @@ -73,10 +73,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 } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt index 482c705e0343..9bbc6f48cd36 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/BackupManager.kt @@ -64,7 +64,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") @@ -81,7 +84,10 @@ 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 @@ -116,7 +122,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 @@ -133,27 +142,34 @@ open class BackupManager { * @return last date in parsable file names or null if all names can't be parsed * Expects a sorted array of backups, as returned by getBackups() */ - fun getLastBackupDate(files: Array): Date? { - return files.lastOrNull()?.let { + fun getLastBackupDate(files: Array): Date? = + files.lastOrNull()?.let { getBackupDate(it.name) } - } - fun getBackupFile(colFile: File, backupFilename: String): File { - return File(getBackupDirectory(colFile.parentFile!!), backupFilename) - } + fun getBackupFile( + colFile: File, + backupFilename: String, + ): File = 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 { @@ -164,17 +180,19 @@ 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 } @@ -186,21 +204,16 @@ open class BackupManager { } } - fun collectionIsTooSmallToBeValid(colFile: File): Boolean { - return ( + fun collectionIsTooSmallToBeValid(colFile: File): Boolean = + ( colFile.length() < MIN_BACKUP_COL_SIZE - ) - } + ) - fun hasFreeDiscSpace(colFile: File): Boolean { - return getFreeDiscSpace(colFile) >= getRequiredFreeSpace(colFile) - } + fun hasFreeDiscSpace(colFile: File): Boolean = getFreeDiscSpace(colFile) >= getRequiredFreeSpace(colFile) @VisibleForTesting - fun hasDisabledBackups(prefs: SharedPreferences): Boolean { - return prefs.getInt("backupMax", 8) == 0 - } + fun hasDisabledBackups(prefs: SharedPreferences): Boolean = prefs.getInt("backupMax", 8) == 0 companion object { /** @@ -225,9 +238,7 @@ open class BackupManager { return directory } - fun getBackupDirectoryFromCollection(colPath: String): String { - return getBackupDirectory(File(colPath).parentFile!!).absolutePath - } + fun getBackupDirectoryFromCollection(colPath: String): String = getBackupDirectory(File(colPath).parentFile!!).absolutePath private fun getBrokenDirectory(ankidroidDir: File): File { val directory = File(ankidroidDir, BROKEN_COLLECTIONS_SUFFIX) @@ -246,20 +257,14 @@ open class BackupManager { return colFile.length() + MIN_FREE_SPACE * 1024 * 1024 } - fun enoughDiscSpace(path: String?): Boolean { - return getFreeDiscSpace(path) >= MIN_FREE_SPACE * 1024 * 1024 - } + fun enoughDiscSpace(path: String?): Boolean = getFreeDiscSpace(path) >= MIN_FREE_SPACE * 1024 * 1024 /** * Get free disc space in bytes from path to Collection */ - fun getFreeDiscSpace(path: String?): Long { - return getFreeDiscSpace(File(path!!)) - } + fun getFreeDiscSpace(path: String?): Long = getFreeDiscSpace(File(path!!)) - private fun getFreeDiscSpace(file: File): Long { - return getFreeDiskSpace(file, (MIN_FREE_SPACE * 1024 * 1024).toLong()) - } + private fun getFreeDiscSpace(file: File): Long = getFreeDiskSpace(file, (MIN_FREE_SPACE * 1024 * 1024).toLong()) /** * Run the sqlite3 command-line-tool (if it exists) on the collection to dump to a text file @@ -300,27 +305,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 @@ -347,15 +358,13 @@ open class BackupManager { * @param fileName String with pattern "collection-yyyy-MM-dd-HH-mm.colpkg" * @return Its dateformat parsable string or null if it doesn't match naming pattern */ - fun getBackupTimeString(fileName: String): String? { - return backupNameRegex.matchEntire(fileName)?.groupValues?.get(1) - } + fun getBackupTimeString(fileName: String): String? = backupNameRegex.matchEntire(fileName)?.groupValues?.get(1) /** * @return date in string if it matches backup naming pattern or null if not */ - fun parseBackupTimeString(timeString: String): Date? { - return try { + fun parseBackupTimeString(timeString: String): Date? = + try { legacyDateFormat.parse(timeString) } catch (e: ParseException) { try { @@ -364,14 +373,11 @@ open class BackupManager { null } } - } /** * @return date in fileName if it matches backup naming pattern or null if not */ - fun getBackupDate(fileName: String): Date? { - return getBackupTimeString(fileName)?.let { parseBackupTimeString(it) } - } + fun getBackupDate(fileName: String): Date? = getBackupTimeString(fileName)?.let { parseBackupTimeString(it) } /** * @return filename with pattern collection-yyyy-MM-dd-HH-mm based on given time parameter @@ -380,12 +386,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 } @@ -395,14 +402,14 @@ 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) - } - } - .sortedBy { it.first } - .map { it.second } + val backups = + files + .mapNotNull { file -> + getBackupTimeString(file.name)?.let { time -> + Pair(time, file) + } + }.sortedBy { it.first } + .map { it.second } return backups.toTypedArray() } @@ -424,25 +431,24 @@ open class BackupManager { fun deleteColBackups( colPath: String, backupLimits: BackupLimits, - today: LocalDate = LocalDate.now() - ): Boolean { - return deleteColBackups(getBackups(File(colPath)), backupLimits, today) - } + today: LocalDate = LocalDate.now(), + ): Boolean = deleteColBackups(getBackups(File(colPath)), backupLimits, today) 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) @@ -460,7 +466,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() @@ -482,9 +491,7 @@ open class BackupManager { } @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun createInstance(): BackupManager { - return BackupManager() - } + fun createInstance(): BackupManager = BackupManager() } } @@ -494,9 +501,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 @@ -506,9 +514,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) @@ -519,11 +528,16 @@ 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 -private class BackupFilter(private val today: LocalDate, private var limits: BackupLimits) { +private class BackupFilter( + private val today: LocalDate, + private var limits: BackupLimits, +) { private val epoch = LocalDate.ofEpochDay(0) private var lastKeptDay: Long = ChronoUnit.DAYS.between(epoch, today) private var lastKeptWeek: Long = ChronoUnit.WEEKS.between(epoch, today) @@ -549,18 +563,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 { @@ -569,7 +588,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 d575b38fba35..070540f590db 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -163,7 +163,6 @@ open class CardBrowser : TagsDialogListener, ChangeManager.Subscriber, ExportDialogsFactoryProvider { - override fun onDeckSelected(deck: SelectableDeck?) { deck?.let { launchCatchingTask { selectDeckAndSave(deck.deckId) } @@ -171,7 +170,8 @@ open class CardBrowser : } private enum class TagsDialogListenerAction { - FILTER, EDIT_TAGS + FILTER, + EDIT_TAGS, } lateinit var viewModel: CardBrowserViewModel @@ -202,75 +202,82 @@ open class CardBrowser : // card that was clicked (not marked) override var currentCardId get() = viewModel.currentCardId - set(value) { viewModel.currentCardId = value } + set(value) { + viewModel.currentCardId = value + } // DEFECT: Doesn't need to be a local private var tagsDialogListenerAction: TagsDialogListenerAction? = null - private var onEditCardActivityResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> - Timber.d("onEditCardActivityResult: resultCode=%d", result.resultCode) - if (result.resultCode == DeckPicker.RESULT_DB_ERROR) { - closeCardBrowser(DeckPicker.RESULT_DB_ERROR) - } - if (result.resultCode != RESULT_CANCELED) { - Timber.i("CardBrowser:: CardBrowser: Saving card...") - saveEditedCard() - } - val data = result.data - if (data != null && - ( - data.getBooleanExtra(NoteEditor.RELOAD_REQUIRED_EXTRA_KEY, false) || - data.getBooleanExtra(NoteEditor.NOTE_CHANGED_EXTRA_KEY, false) + private var onEditCardActivityResult = + registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> + Timber.d("onEditCardActivityResult: resultCode=%d", result.resultCode) + if (result.resultCode == DeckPicker.RESULT_DB_ERROR) { + closeCardBrowser(DeckPicker.RESULT_DB_ERROR) + } + if (result.resultCode != RESULT_CANCELED) { + Timber.i("CardBrowser:: CardBrowser: Saving card...") + saveEditedCard() + } + val data = result.data + if (data != null && + ( + data.getBooleanExtra(NoteEditor.RELOAD_REQUIRED_EXTRA_KEY, false) || + data.getBooleanExtra(NoteEditor.NOTE_CHANGED_EXTRA_KEY, false) ) - ) { - Timber.d("Reloading Card Browser due to activity result") - // if reloadRequired or noteChanged flag was sent from note editor then reload card list - shouldRestoreScroll = true - forceRefreshSearch() - // in use by reviewer? - if (reviewerCardId == currentCardId) { - reloadRequired = true + ) { + Timber.d("Reloading Card Browser due to activity result") + // if reloadRequired or noteChanged flag was sent from note editor then reload card list + shouldRestoreScroll = true + forceRefreshSearch() + // in use by reviewer? + if (reviewerCardId == currentCardId) { + reloadRequired = true + } } + invalidateOptionsMenu() // maybe the availability of undo changed } - invalidateOptionsMenu() // maybe the availability of undo changed - } - private var onAddNoteActivityResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> - Timber.d("onAddNoteActivityResult: resultCode=%d", result.resultCode) - if (result.resultCode == DeckPicker.RESULT_DB_ERROR) { - closeCardBrowser(DeckPicker.RESULT_DB_ERROR) - } - if (result.resultCode == RESULT_OK) { - forceRefreshSearch(useSearchTextValue = true) - } - invalidateOptionsMenu() // maybe the availability of undo changed - } - private var onPreviewCardsActivityResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> - Timber.d("onPreviewCardsActivityResult: resultCode=%d", result.resultCode) - if (result.resultCode == DeckPicker.RESULT_DB_ERROR) { - closeCardBrowser(DeckPicker.RESULT_DB_ERROR) + private var onAddNoteActivityResult = + registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> + Timber.d("onAddNoteActivityResult: resultCode=%d", result.resultCode) + if (result.resultCode == DeckPicker.RESULT_DB_ERROR) { + closeCardBrowser(DeckPicker.RESULT_DB_ERROR) + } + if (result.resultCode == RESULT_OK) { + forceRefreshSearch(useSearchTextValue = true) + } + invalidateOptionsMenu() // maybe the availability of undo changed } - // Previewing can now perform an "edit", so it can pass on a reloadRequired - val data = result.data - if (data != null && - ( - data.getBooleanExtra(NoteEditor.RELOAD_REQUIRED_EXTRA_KEY, false) || - data.getBooleanExtra(NoteEditor.NOTE_CHANGED_EXTRA_KEY, false) + private var onPreviewCardsActivityResult = + registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> + Timber.d("onPreviewCardsActivityResult: resultCode=%d", result.resultCode) + if (result.resultCode == DeckPicker.RESULT_DB_ERROR) { + closeCardBrowser(DeckPicker.RESULT_DB_ERROR) + } + // Previewing can now perform an "edit", so it can pass on a reloadRequired + val data = result.data + if (data != null && + ( + data.getBooleanExtra(NoteEditor.RELOAD_REQUIRED_EXTRA_KEY, false) || + data.getBooleanExtra(NoteEditor.NOTE_CHANGED_EXTRA_KEY, false) ) - ) { - forceRefreshSearch() - if (reviewerCardId == currentCardId) { - reloadRequired = true + ) { + forceRefreshSearch() + if (reviewerCardId == currentCardId) { + reloadRequired = true + } } + invalidateOptionsMenu() // maybe the availability of undo changed } - invalidateOptionsMenu() // maybe the availability of undo changed - } private var lastRenderStart: Long = 0 private lateinit var actionBarTitle: TextView private var reloadRequired = false private var lastSelectedPosition get() = viewModel.lastSelectedPosition - set(value) { viewModel.lastSelectedPosition = value } + set(value) { + viewModel.lastSelectedPosition = value + } private var actionBarMenu: Menu? = null private var oldCardId: CardId = 0 private var oldCardTopOffset = 0 @@ -282,59 +289,64 @@ open class CardBrowser : } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - fun changeCardOrder(sortType: SortType) = launchCatchingTask { - // TODO: remove withProgress and replace with search progress bar - withProgress { viewModel.changeCardOrder(sortType)?.join() } - } + fun changeCardOrder(sortType: SortType) = + launchCatchingTask { + // TODO: remove withProgress and replace with search progress bar + withProgress { viewModel.changeCardOrder(sortType)?.join() } + } @VisibleForTesting - internal val mySearchesDialogListener: MySearchesDialogListener = object : MySearchesDialogListener { - - override fun onSelection(searchName: String) { - Timber.d("OnSelection using search named: %s", searchName) - launchCatchingTask { - viewModel.savedSearches()[searchName]?.also { savedSearch -> - Timber.d("OnSelection using search terms: %s", savedSearch) - searchForQuery(savedSearch) + internal val mySearchesDialogListener: MySearchesDialogListener = + object : MySearchesDialogListener { + override fun onSelection(searchName: String) { + Timber.d("OnSelection using search named: %s", searchName) + launchCatchingTask { + viewModel.savedSearches()[searchName]?.also { savedSearch -> + Timber.d("OnSelection using search terms: %s", savedSearch) + searchForQuery(savedSearch) + } } } - } - override fun onRemoveSearch(searchName: String) { - Timber.d("OnRemoveSelection using search named: %s", searchName) - launchCatchingTask { - val updatedFilters = viewModel.removeSavedSearch(searchName) - if (updatedFilters.isEmpty()) { - mySearchesItem!!.isVisible = false + override fun onRemoveSearch(searchName: String) { + Timber.d("OnRemoveSelection using search named: %s", searchName) + launchCatchingTask { + val updatedFilters = viewModel.removeSavedSearch(searchName) + if (updatedFilters.isEmpty()) { + mySearchesItem!!.isVisible = false + } } } - } - override fun onSaveSearch(searchName: String, searchTerms: String?) { - if (searchTerms == null) { - return - } - if (searchName.isEmpty()) { - showSnackbar( - R.string.card_browser_list_my_searches_new_search_error_empty_name, - Snackbar.LENGTH_SHORT - ) - return - } - launchCatchingTask { - when (viewModel.saveSearch(searchName, searchTerms)) { - SaveSearchResult.ALREADY_EXISTS -> showSnackbar( - R.string.card_browser_list_my_searches_new_search_error_dup, - Snackbar.LENGTH_SHORT + override fun onSaveSearch( + searchName: String, + searchTerms: String?, + ) { + if (searchTerms == null) { + return + } + if (searchName.isEmpty()) { + showSnackbar( + R.string.card_browser_list_my_searches_new_search_error_empty_name, + Snackbar.LENGTH_SHORT, ) - SaveSearchResult.SUCCESS -> { - searchView!!.setQuery("", false) - mySearchesItem!!.isVisible = true + return + } + launchCatchingTask { + when (viewModel.saveSearch(searchName, searchTerms)) { + SaveSearchResult.ALREADY_EXISTS -> + showSnackbar( + R.string.card_browser_list_my_searches_new_search_error_dup, + Snackbar.LENGTH_SHORT, + ) + SaveSearchResult.SUCCESS -> { + searchView!!.setQuery("", false) + mySearchesItem!!.isVisible = true + } } } } } - } @MainThread @NeedsTest("search bar is set after selecting a saved search as first action") @@ -344,9 +356,7 @@ open class CardBrowser : searchView!!.setQuery(query, submit = true) } - private fun canPerformCardInfo(): Boolean { - return viewModel.selectedRowCount() == 1 - } + private fun canPerformCardInfo(): Boolean = viewModel.selectedRowCount() == 1 private fun canPerformMultiSelectEditNote(): Boolean { // The noteId is not currently available. Only allow if a single card is selected for now. @@ -358,12 +368,11 @@ open class CardBrowser : * @param did Id of the deck */ @VisibleForTesting - fun moveSelectedCardsToDeck(did: DeckId): Job { - return launchCatchingTask { + fun moveSelectedCardsToDeck(did: DeckId): Job = + launchCatchingTask { val changed = withProgress { viewModel.moveSelectedCardsToDeck(did).await() } showUndoSnackbar(TR.browsingCardsUpdated(changed.count)) } - } override fun onCreate(savedInstanceState: Bundle?) { if (showedActivityFailedScreen(savedInstanceState)) { @@ -389,30 +398,33 @@ open class CardBrowser : // get the font and font size from the preferences val sflRelativeFontSize = preferences.getInt("relativeCardBrowserFontSize", DEFAULT_FONT_SIZE_RATIO) - val columnsContent = arrayOf( - viewModel.column1, - viewModel.column2 - ) + val columnsContent = + arrayOf( + viewModel.column1, + viewModel.column2, + ) // make a new list adapter mapping the data in mCards to column1 and column2 of R.layout.card_item_browser - cardsAdapter = MultiColumnListAdapter( - this, - R.layout.card_item_browser, - columnsContent, - intArrayOf(R.id.card_sfld, R.id.card_column2), - sflRelativeFontSize - ) + cardsAdapter = + MultiColumnListAdapter( + this, + R.layout.card_item_browser, + columnsContent, + intArrayOf(R.id.card_sfld, R.id.card_column2), + sflRelativeFontSize, + ) // link the adapter to the main mCardsListView cardsListView.adapter = cardsAdapter // make the items (e.g. question & answer) render dynamically when scrolling cardsListView.setOnScrollListener(RenderOnScroll()) - deckSpinnerSelection = DeckSpinnerSelection( - this, - findViewById(R.id.toolbar_spinner), - showAllDecks = true, - alwaysShowDefault = false, - showFilteredDecks = true - ) + deckSpinnerSelection = + DeckSpinnerSelection( + this, + findViewById(R.id.toolbar_spinner), + showAllDecks = true, + alwaysShowDefault = false, + showFilteredDecks = true, + ) updateNumCardsToRender() @@ -437,6 +449,7 @@ open class CardBrowser : private fun setupFlows() { // provides a name for each flow receiver to improve stack traces fun onIsTruncatedChanged(isTruncated: Boolean) = cardsAdapter.notifyDataSetChanged() + fun onSearchQueryExpanded(searchQueryExpanded: Boolean) { Timber.d("query expansion changed: %b", searchQueryExpanded) if (searchQueryExpanded) { @@ -447,7 +460,9 @@ open class CardBrowser : invalidateOptionsMenu() } } + fun onSelectedRowsChanged(rows: Set) = onSelectionChanged() + fun onColumn1Changed(column: CardBrowserColumn) { cardsAdapter.updateMapping { it[0] = column } findViewById(R.id.browser_column1_spinner) @@ -465,14 +480,17 @@ open class CardBrowser : searchItem!!.expandActionView() searchView!!.setQuery(filterQuery, submit = false) } + suspend fun onDeckIdChanged(deckId: DeckId?) { if (deckId == null) return // this handles ALL_DECKS_ID deckSpinnerSelection.selectDeckById(deckId, false) } + fun onCanSaveChanged(canSave: Boolean) { saveSearchItem?.isVisible = canSave } + fun isInMultiSelectModeChanged(inMultiSelect: Boolean) { if (inMultiSelect) { // Turn on Multi-Select Mode so that the user can select multiple cards at once. @@ -493,7 +511,9 @@ open class CardBrowser : // reload the actionbar using the multi-select mode actionbar invalidateOptionsMenu() } + fun cardsUpdatedChanged(unit: Unit) = cardsAdapter.notifyDataSetChanged() + fun searchStateChanged(searchState: SearchState) { Timber.d("search state: %s", searchState) when (searchState) { @@ -515,34 +535,38 @@ open class CardBrowser : fun setupColumnSpinners() { // Create a spinner for column 1 findViewById(R.id.browser_column1_spinner).apply { - adapter = ArrayAdapter( - this@CardBrowser, - android.R.layout.simple_spinner_item, - viewModel.column1Candidates.map { it.getLabel(viewModel.cardsOrNotes) } - ).apply { - setDropDownViewResource(R.layout.spinner_custom_layout) - } + adapter = + ArrayAdapter( + this@CardBrowser, + android.R.layout.simple_spinner_item, + viewModel.column1Candidates.map { it.getLabel(viewModel.cardsOrNotes) }, + ).apply { + setDropDownViewResource(R.layout.spinner_custom_layout) + } setSelection(COLUMN1_KEYS.indexOf(viewModel.column1)) - onItemSelectedListener = BasicItemSelectedListener { pos, _ -> - viewModel.setColumn1(COLUMN1_KEYS[pos]) - } + onItemSelectedListener = + BasicItemSelectedListener { pos, _ -> + viewModel.setColumn1(COLUMN1_KEYS[pos]) + } } // Setup the column 2 heading as a spinner so that users can easily change the column type findViewById(R.id.browser_column2_spinner).apply { - adapter = ArrayAdapter( - this@CardBrowser, - android.R.layout.simple_spinner_item, - viewModel.column2Candidates.map { it.getLabel(viewModel.cardsOrNotes) } - ).apply { - // The custom layout for the adapter is used to prevent the overlapping of various interactive components on the screen - setDropDownViewResource(R.layout.spinner_custom_layout) - } + adapter = + ArrayAdapter( + this@CardBrowser, + android.R.layout.simple_spinner_item, + viewModel.column2Candidates.map { it.getLabel(viewModel.cardsOrNotes) }, + ).apply { + // The custom layout for the adapter is used to prevent the overlapping of various interactive components on the screen + setDropDownViewResource(R.layout.spinner_custom_layout) + } setSelection(COLUMN2_KEYS.indexOf(viewModel.column2)) // Create a new list adapter with updated column map any time the user changes the column - onItemSelectedListener = BasicItemSelectedListener { pos, _ -> - viewModel.setColumn2(COLUMN2_KEYS[pos]) - } + onItemSelectedListener = + BasicItemSelectedListener { pos, _ -> + viewModel.setColumn2(COLUMN2_KEYS[pos]) + } } } @@ -634,7 +658,10 @@ open class CardBrowser : viewModel.setDeckId(deckId) } - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { // This method is called even when the user is typing in the search text field. // So we must ensure that all shortcuts uses a modifier. // A shortcut without modifier would be triggered while the user types, which is not what we want. @@ -814,16 +841,17 @@ open class CardBrowser : } private fun updateFlag(keyCode: Int) { - val flag = when (keyCode) { - KeyEvent.KEYCODE_1 -> Flag.RED - KeyEvent.KEYCODE_2 -> Flag.ORANGE - KeyEvent.KEYCODE_3 -> Flag.GREEN - KeyEvent.KEYCODE_4 -> Flag.BLUE - KeyEvent.KEYCODE_5 -> Flag.PINK - KeyEvent.KEYCODE_6 -> Flag.TURQUOISE - KeyEvent.KEYCODE_7 -> Flag.PURPLE - else -> return - } + val flag = + when (keyCode) { + KeyEvent.KEYCODE_1 -> Flag.RED + KeyEvent.KEYCODE_2 -> Flag.ORANGE + KeyEvent.KEYCODE_3 -> Flag.GREEN + KeyEvent.KEYCODE_4 -> Flag.BLUE + KeyEvent.KEYCODE_5 -> Flag.PINK + KeyEvent.KEYCODE_6 -> Flag.TURQUOISE + KeyEvent.KEYCODE_7 -> Flag.PURPLE + else -> return + } updateFlagForSelectedRows(flag) } @@ -832,10 +860,11 @@ open class CardBrowser : * otherwise, they will be unmarked */ @NeedsTest("Test that the mark get toggled as expected for a list of selected cards") @VisibleForTesting - fun toggleMark() = launchCatchingTask { - withProgress { viewModel.toggleMark() } - cardsAdapter.notifyDataSetChanged() - } + fun toggleMark() = + launchCatchingTask { + withProgress { viewModel.toggleMark() } + cardsAdapter.notifyDataSetChanged() + } /** Opens the note editor for a card. * We use the Card ID to specify the preview target */ @@ -861,27 +890,28 @@ open class CardBrowser : } } - private fun openNoteEditorForCurrentlySelectedNote() = launchCatchingTask { - // Check whether the deck is empty - if (viewModel.rowCount == 0) { - showSnackbar( - R.string.no_note_to_edit, - Snackbar.LENGTH_LONG - ) - return@launchCatchingTask - } + private fun openNoteEditorForCurrentlySelectedNote() = + launchCatchingTask { + // Check whether the deck is empty + if (viewModel.rowCount == 0) { + showSnackbar( + R.string.no_note_to_edit, + Snackbar.LENGTH_LONG, + ) + return@launchCatchingTask + } - try { - val cardId = getCardIdForNoteEditor() - openNoteEditorForCard(cardId) - } catch (e: Exception) { - Timber.w(e, "Error Opening Note Editor") - showSnackbar( - R.string.multimedia_editor_something_wrong, - Snackbar.LENGTH_LONG - ) + try { + val cardId = getCardIdForNoteEditor() + openNoteEditorForCard(cardId) + } catch (e: Exception) { + Timber.w(e, "Error Opening Note Editor") + showSnackbar( + R.string.multimedia_editor_something_wrong, + Snackbar.LENGTH_LONG, + ) + } } - } override fun onStop() { // cancel rendering the question and answer, which has shared access to mCards @@ -941,8 +971,7 @@ open class CardBrowser : // restore drawer click listener and icon restoreDrawerIcon() menuInflater.inflate(R.menu.card_browser, menu) - menu.findItem(R.id.action_search_by_flag).subMenu?.let { - subMenu -> + menu.findItem(R.id.action_search_by_flag).subMenu?.let { subMenu -> setupFlags(subMenu, Mode.SINGLE_SELECT) } menu.findItem(R.id.action_create_filtered_deck).title = TR.qtMiscCreateFilteredDeck() @@ -952,39 +981,44 @@ open class CardBrowser : val savedFiltersObj = viewModel.savedSearchesUnsafe(getColUnsafe) mySearchesItem!!.isVisible = savedFiltersObj.size > 0 searchItem = menu.findItem(R.id.action_search) - searchItem!!.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - viewModel.setSearchQueryExpanded(true) - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - viewModel.setSearchQueryExpanded(false) - // SearchView doesn't support empty queries so we always reset the search when collapsing - searchView!!.setQuery("", false) - searchCards("") - return true - } - }) - searchView = (searchItem!!.actionView as CardBrowserSearchView).apply { - queryHint = resources.getString(R.string.card_browser_search_hint) - setMaxWidth(Integer.MAX_VALUE) - setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextChange(newText: String): Boolean { - if (this@apply.ignoreValueChange) { - return true - } - viewModel.updateQueryText(newText) + searchItem!!.setOnActionExpandListener( + object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + viewModel.setSearchQueryExpanded(true) return true } - override fun onQueryTextSubmit(query: String): Boolean { - searchCards(query) - searchView!!.clearFocus() + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + viewModel.setSearchQueryExpanded(false) + // SearchView doesn't support empty queries so we always reset the search when collapsing + searchView!!.setQuery("", false) + searchCards("") return true } - }) - } + }, + ) + searchView = + (searchItem!!.actionView as CardBrowserSearchView).apply { + queryHint = resources.getString(R.string.card_browser_search_hint) + setMaxWidth(Integer.MAX_VALUE) + setOnQueryTextListener( + object : SearchView.OnQueryTextListener { + override fun onQueryTextChange(newText: String): Boolean { + if (this@apply.ignoreValueChange) { + return true + } + viewModel.updateQueryText(newText) + return true + } + + override fun onQueryTextSubmit(query: String): Boolean { + searchCards(query) + searchView!!.clearFocus() + return true + } + }, + ) + } // Fixes #6500 - keep the search consistent if coming back from note editor // Fixes #9010 - consistent search after drawer change calls invalidateOptionsMenu if (!viewModel.tempSearchQuery.isNullOrEmpty() || viewModel.searchTerms.isNotEmpty()) { @@ -999,8 +1033,7 @@ open class CardBrowser : } else { // multi-select mode menuInflater.inflate(R.menu.card_browser_multiselect, menu) - menu.findItem(R.id.action_flag).subMenu?.let { - subMenu -> + menu.findItem(R.id.action_flag).subMenu?.let { subMenu -> setupFlags(subMenu, Mode.MULTI_SELECT) } showBackIcon() @@ -1023,20 +1056,27 @@ open class CardBrowser : /** * Representing different selection modes. */ - enum class Mode(val value: Int) { + enum class Mode( + val value: Int, + ) { SINGLE_SELECT(1000), - MULTI_SELECT(1001) + MULTI_SELECT(1001), } - private fun setupFlags(subMenu: SubMenu, mode: Mode) { + private fun setupFlags( + subMenu: SubMenu, + mode: Mode, + ) { lifecycleScope.launch { - val groupId = when (mode) { - Mode.SINGLE_SELECT -> mode.value - Mode.MULTI_SELECT -> mode.value - } + val groupId = + when (mode) { + Mode.SINGLE_SELECT -> mode.value + Mode.MULTI_SELECT -> mode.value + } for ((flag, displayName) in Flag.queryDisplayNames()) { - subMenu.add(groupId, flag.code, Menu.NONE, displayName) + subMenu + .add(groupId, flag.code, Menu.NONE, displayName) .setIcon(flag.drawableRes) } } @@ -1075,23 +1115,25 @@ open class CardBrowser : } } actionBarMenu.findItem(R.id.action_export_selected).apply { - this.title = if (viewModel.cardsOrNotes == CARDS) { - resources.getQuantityString( - R.plurals.card_browser_export_cards, - viewModel.selectedRowCount() - ) - } else { - resources.getQuantityString( - R.plurals.card_browser_export_notes, - viewModel.selectedRowCount() - ) - } + this.title = + if (viewModel.cardsOrNotes == CARDS) { + resources.getQuantityString( + R.plurals.card_browser_export_cards, + viewModel.selectedRowCount(), + ) + } else { + resources.getQuantityString( + R.plurals.card_browser_export_notes, + viewModel.selectedRowCount(), + ) + } } actionBarMenu.findItem(R.id.action_delete_card).apply { - this.title = resources.getQuantityString( - R.plurals.card_browser_delete_notes, - viewModel.selectedNoteCount() - ) + this.title = + resources.getQuantityString( + R.plurals.card_browser_delete_notes, + viewModel.selectedNoteCount(), + ) } actionBarMenu.findItem(R.id.action_select_all).isVisible = !hasSelectedAllCards() // Note: Theoretically should not happen, as this should kick us back to the menu @@ -1105,9 +1147,10 @@ open class CardBrowser : return viewModel.selectedRowCount() >= viewModel.rowCount // must handle 0. } - private fun updateFlagForSelectedRows(flag: Flag) = launchCatchingTask { - updateSelectedCardsFlag(flag) - } + private fun updateFlagForSelectedRows(flag: Flag) = + launchCatchingTask { + updateSelectedCardsFlag(flag) + } /** * Sets the flag for selected cards, default norm of flags are as: @@ -1140,6 +1183,8 @@ open class CardBrowser : return true } + @NeedsTest("filter-marked query needs testing") + @NeedsTest("filter-suspended query needs testing") override fun onOptionsItemSelected(item: MenuItem): Boolean { when { drawerToggle.onOptionsItemSelected(item) -> return true @@ -1179,14 +1224,10 @@ open class CardBrowser : changeDisplayOrder() return true } - - @NeedsTest("filter-marked query needs testing") R.id.action_show_marked -> { searchForMarkedNotes() return true } - - @NeedsTest("filter-suspended query needs testing") R.id.action_show_suspended -> { searchForSuspendedCards() return true @@ -1304,7 +1345,7 @@ open class CardBrowser : CardBrowserOrderDialog.newInstance { dialog: DialogInterface, which: Int -> dialog.dismiss() changeCardOrder(SortType.fromCardBrowserLabelIndex(which)) - } + }, ) } @@ -1316,8 +1357,8 @@ open class CardBrowser : savedFilters, mySearchesDialogListener, "", - CardBrowserMySearchesDialog.CARD_BROWSER_MY_SEARCHES_TYPE_LIST - ) + CardBrowserMySearchesDialog.CARD_BROWSER_MY_SEARCHES_TYPE_LIST, + ), ) } } @@ -1329,8 +1370,8 @@ open class CardBrowser : null, mySearchesDialogListener, searchTerms, - CardBrowserMySearchesDialog.CARD_BROWSER_MY_SEARCHES_TYPE_SAVE - ) + CardBrowserMySearchesDialog.CARD_BROWSER_MY_SEARCHES_TYPE_SAVE, + ), ) } @@ -1345,19 +1386,20 @@ open class CardBrowser : SimpleMessageDialog.newInstance( title = getString(R.string.vague_error), message = getString(R.string.reposition_card_not_new_error), - reload = false - ) + reload = false, + ), ) return@launchCatchingTask } - val repositionDialog = IntegerDialog().apply { - setArgs( - title = this@CardBrowser.getString(R.string.reposition_card_dialog_title), - prompt = this@CardBrowser.getString(R.string.reposition_card_dialog_message), - digits = 5 - ) - setCallbackRunnable(::repositionCardsNoValidation) - } + val repositionDialog = + IntegerDialog().apply { + setArgs( + title = this@CardBrowser.getString(R.string.reposition_card_dialog_title), + prompt = this@CardBrowser.getString(R.string.reposition_card_dialog_message), + digits = 5, + ) + setCallbackRunnable(::repositionCardsNoValidation) + } showDialogFragment(repositionDialog) } return true @@ -1374,19 +1416,21 @@ open class CardBrowser : override fun exportDialogsFactory(): ExportDialogsFactory = exportingDelegate.dialogsFactory - private fun exportSelected() = launchCatchingTask { - val (type, selectedIds) = viewModel.querySelectionExportData() ?: return@launchCatchingTask - ExportDialogFragment.newInstance(type, selectedIds).show(supportFragmentManager, "exportDialog") - } + private fun exportSelected() = + launchCatchingTask { + val (type, selectedIds) = viewModel.querySelectionExportData() ?: return@launchCatchingTask + ExportDialogFragment.newInstance(type, selectedIds).show(supportFragmentManager, "exportDialog") + } - private fun deleteSelectedNotes() = launchCatchingTask { - withProgress(R.string.deleting_selected_notes) { - viewModel.deleteSelectedNotes() - }.ifNotZero { noteCount -> - val deletedMessage = resources.getQuantityString(R.plurals.card_browser_cards_deleted, noteCount, noteCount) - showUndoSnackbar(deletedMessage) + private fun deleteSelectedNotes() = + launchCatchingTask { + withProgress(R.string.deleting_selected_notes) { + viewModel.deleteSelectedNotes() + }.ifNotZero { noteCount -> + val deletedMessage = resources.getQuantityString(R.plurals.card_browser_cards_deleted, noteCount, noteCount) + showUndoSnackbar(deletedMessage) + } } - } @VisibleForTesting fun onUndo() { @@ -1401,17 +1445,18 @@ open class CardBrowser : } @VisibleForTesting - fun repositionCardsNoValidation(position: Int) = launchCatchingTask { - val count = withProgress { viewModel.repositionSelectedRows(position) } - showSnackbar( - resources.getQuantityString( - R.plurals.reposition_card_dialog_acknowledge, - count, - count - ), - Snackbar.LENGTH_SHORT - ) - } + fun repositionCardsNoValidation(position: Int) = + launchCatchingTask { + val count = withProgress { viewModel.repositionSelectedRows(position) } + showSnackbar( + resources.getQuantityString( + R.plurals.reposition_card_dialog_acknowledge, + count, + count, + ), + Snackbar.LENGTH_SHORT, + ) + } private fun onPreview() { launchCatchingTask { @@ -1420,9 +1465,10 @@ open class CardBrowser : } } - private fun getPreviewIntent(index: Int, previewerIdsFile: PreviewerIdsFile): Intent { - return PreviewerDestination(index, previewerIdsFile).toIntent(this) - } + private fun getPreviewIntent( + index: Int, + previewerIdsFile: PreviewerIdsFile, + ): Intent = PreviewerDestination(index, previewerIdsFile).toIntent(this) private fun rescheduleSelectedCards() { if (!viewModel.hasSelectedAnyRows()) { @@ -1439,12 +1485,13 @@ open class CardBrowser : @KotlinCleanup("DeckSelectionListener is almost certainly a bug - deck!!") fun getChangeDeckDialog(selectableDecks: List?): DeckSelectionDialog { - val dialog = newInstance( - getString(R.string.move_all_to_deck), - null, - false, - selectableDecks!! - ) + val dialog = + newInstance( + getString(R.string.move_all_to_deck), + null, + false, + selectableDecks!!, + ) // Add change deck argument so the dialog can be dismissed // after activity recreation, since the selected cards will be gone with it dialog.requireArguments().putBoolean(CHANGE_DECK_KEY, true) @@ -1452,16 +1499,18 @@ open class CardBrowser : return dialog } - private fun showChangeDeckDialog() = launchCatchingTask { - if (!viewModel.hasSelectedAnyRows()) { - Timber.i("Not showing Change Deck - No Cards") - return@launchCatchingTask + private fun showChangeDeckDialog() = + launchCatchingTask { + if (!viewModel.hasSelectedAnyRows()) { + Timber.i("Not showing Change Deck - No Cards") + return@launchCatchingTask + } + val selectableDecks = + getValidDecksForChangeDeck() + .map { d -> SelectableDeck(d) } + val dialog = getChangeDeckDialog(selectableDecks) + showDialogFragment(dialog) } - val selectableDecks = getValidDecksForChangeDeck() - .map { d -> SelectableDeck(d) } - val dialog = getChangeDeckDialog(selectableDecks) - showDialogFragment(dialog) - } @get:VisibleForTesting val addNoteIntent: Intent @@ -1498,44 +1547,45 @@ open class CardBrowser : progressMax = selectedNoteIds.size * 2 // TODO!! This is terribly slow on AnKing - val checkedTags = withCol { - selectedNoteIds - .asSequence() // reduce memory pressure - .flatMap { nid -> - progress++ - getNote(nid).tags // requires withCol - } - .distinct() - .toList() - } + val checkedTags = + withCol { + selectedNoteIds + .asSequence() // reduce memory pressure + .flatMap { nid -> + progress++ + getNote(nid).tags // requires withCol + }.distinct() + .toList() + } if (selectedNoteIds.size == 1) { Timber.d("showEditTagsDialog: edit tags for one note") tagsDialogListenerAction = TagsDialogListenerAction.EDIT_TAGS - val dialog = tagsDialogFactory.newTagsDialog().withArguments( - this@CardBrowser, - type = TagsDialog.DialogType.EDIT_TAGS, - checkedTags = checkedTags, - allTags = allTags - ) + val dialog = + tagsDialogFactory.newTagsDialog().withArguments( + this@CardBrowser, + type = TagsDialog.DialogType.EDIT_TAGS, + checkedTags = checkedTags, + allTags = allTags, + ) showDialogFragment(dialog) return@withProgress } // TODO!! This is terribly slow on AnKing // PERF: This MUST be combined with the above sequence - this becomes O(2n) on a // database operation performed over 30k times - val uncheckedTags = withCol { - selectedNoteIds - .asSequence() // reduce memory pressure - .flatMap { nid: NoteId -> - progress++ - val note = getNote(nid) // requires withCol - val noteTags = note.tags.toSet() - allTags.filter { t: String? -> !noteTags.contains(t) } - } - .distinct() - .toList() - } + val uncheckedTags = + withCol { + selectedNoteIds + .asSequence() // reduce memory pressure + .flatMap { nid: NoteId -> + progress++ + val note = getNote(nid) // requires withCol + val noteTags = note.tags.toSet() + allTags.filter { t: String? -> !noteTags.contains(t) } + }.distinct() + .toList() + } progressMax = null @@ -1543,15 +1593,16 @@ open class CardBrowser : tagsDialogListenerAction = TagsDialogListenerAction.EDIT_TAGS // withArguments performs IO, can be 18 seconds - val dialog = withContext(Dispatchers.IO) { - tagsDialogFactory.newTagsDialog().withArguments( - context = this@CardBrowser, - type = TagsDialog.DialogType.EDIT_TAGS, - checkedTags = checkedTags, - uncheckedTags = uncheckedTags, - allTags = allTags - ) - } + val dialog = + withContext(Dispatchers.IO) { + tagsDialogFactory.newTagsDialog().withArguments( + context = this@CardBrowser, + type = TagsDialog.DialogType.EDIT_TAGS, + checkedTags = checkedTags, + uncheckedTags = uncheckedTags, + allTags = allTags, + ) + } showDialogFragment(dialog) } } @@ -1559,12 +1610,13 @@ open class CardBrowser : private fun showFilterByTagsDialog() { tagsDialogListenerAction = TagsDialogListenerAction.FILTER - val dialog = tagsDialogFactory.newTagsDialog().withArguments( - context = this@CardBrowser, - type = TagsDialog.DialogType.FILTER_BY_TAG, - checkedTags = ArrayList(0), - allTags = getColUnsafe.tags.all() - ) + val dialog = + tagsDialogFactory.newTagsDialog().withArguments( + context = this@CardBrowser, + type = TagsDialog.DialogType.FILTER_BY_TAG, + checkedTags = ArrayList(0), + allTags = getColUnsafe.tags.all(), + ) showDialogFragment(dialog) } @@ -1620,7 +1672,7 @@ open class CardBrowser : private fun redrawAfterSearch() { Timber.i("CardBrowser:: Completed searchCards() Successfully") updateList() - /*check whether mSearchView is initialized as it is lateinit property.*/ + // check whether mSearchView is initialized as it is lateinit property. if (searchView == null || searchView!!.isIconified) { restoreScrollPositionIfRequested() return @@ -1629,11 +1681,12 @@ open class CardBrowser : showSnackbar(subtitleText, Snackbar.LENGTH_SHORT) } else { // If we haven't selected all decks, allow the user the option to search all decks. - val message = if (viewModel.rowCount == 0) { - getString(R.string.card_browser_no_cards_in_deck, selectedDeckNameForUi) - } else { - subtitleText - } + val message = + if (viewModel.rowCount == 0) { + getString(R.string.card_browser_no_cards_in_deck, selectedDeckNameForUi) + } else { + subtitleText + } showSnackbar(message, Snackbar.LENGTH_INDEFINITE) { setAction(R.string.card_browser_search_all_decks) { searchAllDecks() } } @@ -1663,7 +1716,7 @@ open class CardBrowser : ( cardsListView.height / TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20f, resources.displayMetrics) - ).toDouble() + ).toDouble(), ).toInt() + 5 } @@ -1684,25 +1737,30 @@ open class CardBrowser : get() { val count = viewModel.rowCount - @androidx.annotation.StringRes val subtitleId = if (viewModel.cardsOrNotes == CARDS) { - R.plurals.card_browser_subtitle - } else { - R.plurals.card_browser_subtitle_notes_mode - } + @androidx.annotation.StringRes val subtitleId = + if (viewModel.cardsOrNotes == CARDS) { + R.plurals.card_browser_subtitle + } else { + R.plurals.card_browser_subtitle_notes_mode + } return resources.getQuantityString(subtitleId, count, count) } /** Returns the decks which are valid targets for "Change Deck" */ - suspend fun getValidDecksForChangeDeck(): List = - deckSpinnerSelection.computeDropDownDecks(includeFiltered = false) + suspend fun getValidDecksForChangeDeck(): List = deckSpinnerSelection.computeDropDownDecks(includeFiltered = false) @RustCleanup("this isn't how Desktop Anki does it") - override fun onSelectedTags(selectedTags: List, indeterminateTags: List, stateFilter: CardStateFilter) { + override fun onSelectedTags( + selectedTags: List, + indeterminateTags: List, + stateFilter: CardStateFilter, + ) { when (tagsDialogListenerAction) { TagsDialogListenerAction.FILTER -> filterByTags(selectedTags, stateFilter) - TagsDialogListenerAction.EDIT_TAGS -> launchCatchingTask { - editSelectedCardsTags(selectedTags, indeterminateTags) - } + TagsDialogListenerAction.EDIT_TAGS -> + launchCatchingTask { + editSelectedCardsTags(selectedTags, indeterminateTags) + } else -> {} } } @@ -1713,24 +1771,30 @@ open class CardBrowser : * @param indeterminateTags a list of tags which can checked or unchecked, should be ignored if not expected * For more info on [selectedTags] and [indeterminateTags] see [com.ichi2.anki.dialogs.tags.TagsDialogListener.onSelectedTags] */ - private suspend fun editSelectedCardsTags(selectedTags: List, indeterminateTags: List) = withProgress { + private suspend fun editSelectedCardsTags( + selectedTags: List, + indeterminateTags: List, + ) = withProgress { val selectedNoteIds = viewModel.queryAllSelectedNoteIds().distinct() undoableOp { - val selectedNotes = selectedNoteIds - .map { noteId -> getNote(noteId) } - .onEach { note -> - val previousTags: List = note.tags - val updatedTags = getUpdatedTags(previousTags, selectedTags, indeterminateTags) - note.setTagsFromStr(this@undoableOp, tags.join(updatedTags)) - } + val selectedNotes = + selectedNoteIds + .map { noteId -> getNote(noteId) } + .onEach { note -> + val previousTags: List = note.tags + val updatedTags = getUpdatedTags(previousTags, selectedTags, indeterminateTags) + note.setTagsFromStr(this@undoableOp, tags.join(updatedTags)) + } updateNotes(selectedNotes) } } - private fun filterByTags(selectedTags: List, cardState: CardStateFilter) = - launchCatchingTask { - viewModel.filterByTags(selectedTags, cardState) - } + private fun filterByTags( + selectedTags: List, + cardState: CardStateFilter, + ) = launchCatchingTask { + viewModel.filterByTags(selectedTags, cardState) + } /** Updates search terms to only show cards with selected flag. */ @VisibleForTesting @@ -1760,14 +1824,18 @@ open class CardBrowser : * Removes cards from view. Doesn't delete them in model (database). * @param reorderCards Whether to rearrange the positions of checked items (DEFECT: Currently deselects all) */ - private fun removeNotesView(cardsIds: List, reorderCards: Boolean) { + private fun removeNotesView( + cardsIds: List, + reorderCards: Boolean, + ) { val idToPos = viewModel.cardIdToPositionMap val idToRemove = cardsIds.filter { cId -> idToPos.containsKey(cId) } reloadRequired = reloadRequired || cardsIds.contains(reviewerCardId) - val newMCards: MutableList = cards - .filterNot { c -> idToRemove.contains(c.id) } - .mapIndexed { i, c -> CardCache(c, i) } - .toMutableList() + val newMCards: MutableList = + cards + .filterNot { c -> idToRemove.contains(c.id) } + .mapIndexed { i, c -> CardCache(c, i) } + .toMutableList() cards.replaceWith(newMCards) if (reorderCards) { // Suboptimal from a UX perspective, we should reorder @@ -1782,15 +1850,17 @@ open class CardBrowser : private fun toggleSuspendCards() = launchCatchingTask { withProgress { viewModel.toggleSuspendCards().join() } } /** @see CardBrowserViewModel.toggleBury */ - private fun toggleBury() = launchCatchingTask { - val result = withProgress { viewModel.toggleBury() } ?: return@launchCatchingTask - // show a snackbar as there's currently no colored background for buried cards - val message = when (result.wasBuried) { - true -> TR.studyingCardsBuried(result.count) - false -> resources.getQuantityString(R.plurals.unbury_cards_feedback, result.count, result.count) + private fun toggleBury() = + launchCatchingTask { + val result = withProgress { viewModel.toggleBury() } ?: return@launchCatchingTask + // show a snackbar as there's currently no colored background for buried cards + val message = + when (result.wasBuried) { + true -> TR.studyingCardsBuried(result.count) + false -> resources.getQuantityString(R.plurals.unbury_cards_feedback, result.count, result.count) + } + showUndoSnackbar(message) } - showUndoSnackbar(message) - } private fun showUndoSnackbar(message: CharSequence) { showSnackbar(message, Snackbar.LENGTH_LONG) { @@ -1808,6 +1878,7 @@ open class CardBrowser : updatePreviewMenuItem() invalidateOptionsMenu() // maybe the availability of undo changed } + private suspend fun saveScrollingState(position: Int) { oldCardId = viewModel.queryCardIdAtPosition(position) oldCardTopOffset = calculateTopOffset(position) @@ -1826,10 +1897,11 @@ open class CardBrowser : fun hasSelectedAllDecks(): Boolean = viewModel.lastDeckId == ALL_DECKS_ID - fun searchAllDecks() = launchCatchingTask { - // all we need to do is select all decks - viewModel.setDeckId(ALL_DECKS_ID) - } + fun searchAllDecks() = + launchCatchingTask { + // all we need to do is select all decks + viewModel.setDeckId(ALL_DECKS_ID) + } /** * Returns the current deck name, "All Decks" if all decks are selected, or "Unknown" @@ -1837,16 +1909,17 @@ open class CardBrowser : * with the collection. */ val selectedDeckNameForUi: String - get() = try { - when (val deckId = viewModel.lastDeckId) { - null -> getString(R.string.card_browser_unknown_deck_name) - ALL_DECKS_ID -> getString(R.string.card_browser_all_decks) - else -> getColUnsafe.decks.name(deckId) + get() = + try { + when (val deckId = viewModel.lastDeckId) { + null -> getString(R.string.card_browser_unknown_deck_name) + ALL_DECKS_ID -> getString(R.string.card_browser_all_decks) + else -> getColUnsafe.decks.name(deckId) + } + } catch (e: Exception) { + Timber.w(e, "Unable to get selected deck name") + getString(R.string.card_browser_unknown_deck_name) } - } catch (e: Exception) { - Timber.w(e, "Unable to get selected deck name") - getString(R.string.card_browser_unknown_deck_name) - } private fun onPostExecuteRenderBrowserQA(result: Pair, List>) { val cardsIdsToHide = result.second @@ -1863,7 +1936,10 @@ open class CardBrowser : Timber.d("Completed doInBackgroundRenderBrowserQA Successfully") } - private fun closeCardBrowser(result: Int, data: Intent? = null) { + private fun closeCardBrowser( + result: Int, + data: Intent? = null, + ) { // Set result and finish setResult(result, data) finish() @@ -1874,7 +1950,12 @@ open class CardBrowser : */ @VisibleForTesting inner class RenderOnScroll : AbsListView.OnScrollListener { - override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) { + override fun onScroll( + view: AbsListView, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int, + ) { // Show the progress bar if scrolling to given position requires rendering of the question / answer val lastVisibleItem = firstVisibleItem + visibleItemCount - 1 // List is never cleared, only reset to a new list. So it's safe here. @@ -1895,7 +1976,7 @@ open class CardBrowser : Timber.w( "CardBrowser Scroll Issue 15441/8821: In a search result of $size " + "cards, with totalItemCount = $totalItemCount, " + - "somehow we got $visibleItemCount elements to display." + "somehow we got $visibleItemCount elements to display.", ) } // In all of those cases, there is nothing to do: @@ -1919,7 +2000,10 @@ open class CardBrowser : } } - override fun onScrollStateChanged(listView: AbsListView, scrollState: Int) { + override fun onScrollStateChanged( + listView: AbsListView, + scrollState: Int, + ) { // TODO: Try change to RecyclerView as currently gets stuck a lot when using scrollbar on right of ListView // Start rendering the question & answer every time the user stops scrolling if (postAutoScroll) { @@ -1934,19 +2018,24 @@ open class CardBrowser : } // TODO: Improve progress bar handling in places where this function is used - protected suspend fun renderBrowserQAParams(firstVisibleItem: Int, visibleItemCount: Int, cards: List) { + protected suspend fun renderBrowserQAParams( + firstVisibleItem: Int, + visibleItemCount: Int, + cards: List, + ) { Timber.d("Starting Q&A background rendering") - val result = renderBrowserQA( - cards, - firstVisibleItem, - visibleItemCount, - viewModel.column1, - viewModel.column2 - ) { - // Note: This is called every time a card is rendered. - // It blocks the long-click callback while the task is running, so usage of the task should be minimized - cardsAdapter.notifyDataSetChanged() - } + val result = + renderBrowserQA( + cards, + firstVisibleItem, + visibleItemCount, + viewModel.column1, + viewModel.column2, + ) { + // Note: This is called every time a card is rendered. + // It blocks the long-click callback while the task is running, so usage of the task should be minimized + cardsAdapter.notifyDataSetChanged() + } onPostExecuteRenderBrowserQA(result) } @@ -1956,11 +2045,16 @@ open class CardBrowser : private val resource: Int, private var fromKeys: Array, private val toIds: IntArray, - private val fontSizeScalePcent: Int + private val fontSizeScalePcent: Int, ) : BaseAdapter() { private var originalTextSize = -1.0f private val inflater: LayoutInflater - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { // Get the main container view if it doesn't already exist, and call bindView val v: View if (convertView == null) { @@ -1979,7 +2073,10 @@ open class CardBrowser : } @KotlinCleanup("Unchecked cast") - private fun bindView(position: Int, v: View) { + private fun bindView( + position: Int, + v: View, + ) { // Draw the content in the columns val card = getItem(position) (v.tag as Array<*>) @@ -2046,15 +2143,11 @@ open class CardBrowser : fromMapping = fromMap } - override fun getCount(): Int { - return viewModel.rowCount - } + override fun getCount(): Int = viewModel.rowCount override fun getItem(position: Int): CardCache = viewModel.getRowAtPosition(position) - override fun getItemId(position: Int): Long { - return position.toLong() - } + override fun getItemId(position: Int): Long = position.toLong() init { inflater = LayoutInflater.from(context) @@ -2088,29 +2181,28 @@ open class CardBrowser : * @see showedActivityFailedScreen - we may not have AnkiDroidApp.instance and therefore can't * create the ViewModel */ - private fun createViewModel(launchOptions: CardBrowserLaunchOptions?) = ViewModelProvider( - viewModelStore, - CardBrowserViewModel.factory( - lastDeckIdRepository = AnkiDroidApp.instance.sharedPrefsLastDeckIdRepository, - cacheDir = cacheDir, - options = launchOptions - ), - defaultViewModelCreationExtras - )[CardBrowserViewModel::class.java] + private fun createViewModel(launchOptions: CardBrowserLaunchOptions?) = + ViewModelProvider( + viewModelStore, + CardBrowserViewModel.factory( + lastDeckIdRepository = AnkiDroidApp.instance.sharedPrefsLastDeckIdRepository, + cacheDir = cacheDir, + options = launchOptions, + ), + defaultViewModelCreationExtras, + )[CardBrowserViewModel::class.java] // This could be better: use a wrapper class PositionAware to store the position so it's // no longer a responsibility of CardCache and we can guarantee it's consistent just by using this collection + /** A position-aware collection to ensure consistency between the position of items and the collection */ class CardCollection : Iterable { var wrapped: MutableList = ArrayList(0) private set - fun size(): Int { - return wrapped.size - } - operator fun get(index: Int): T { - return wrapped[index] - } + fun size(): Int = wrapped.size + + operator fun get(index: Int): T = wrapped[index] fun reset() { wrapped = ArrayList(0) @@ -2125,9 +2217,7 @@ open class CardBrowser : wrapped.forEachIndexed { pos, card -> card!!.position = pos } } - override fun iterator(): MutableIterator { - return wrapped.iterator() - } + override fun iterator(): MutableIterator = wrapped.iterator() fun clear() { wrapped.clear() @@ -2139,7 +2229,9 @@ open class CardBrowser : var position: Int } - class CardCache : Card.Cache, PositionAware { + class CardCache : + Card.Cache, + PositionAware { var isLoaded = false private set private var qa: Pair? = null @@ -2175,18 +2267,19 @@ open class CardBrowser : if (flagColor != null) { return context.getColor(flagColor) } - val colorAttr = if (isMarked(col, card.note(col))) { - R.attr.markedColor - } else if (card.queue == Consts.QUEUE_TYPE_SUSPENDED) { - R.attr.suspendedColor - } else { - android.R.attr.colorBackground - } + val colorAttr = + if (isMarked(col, card.note(col))) { + R.attr.markedColor + } else if (card.queue == Consts.QUEUE_TYPE_SUSPENDED) { + R.attr.suspendedColor + } else { + android.R.attr.colorBackground + } return Themes.getColorFromAttr(context, colorAttr) } - fun getColumnHeaderText(key: CardBrowserColumn): String? { - return when (key) { + fun getColumnHeaderText(key: CardBrowserColumn): String? = + when (key) { CardBrowserColumn.SFLD -> card.note(col).sFld(col) CardBrowserColumn.DECK -> col.decks.name(card.did) CardBrowserColumn.TAGS -> card.note(col).stringTags(col) @@ -2211,17 +2304,16 @@ open class CardBrowser : } CardBrowserColumn.FSRS_DIFFICULTY, CardBrowserColumn.FSRS_RETRIEVABILITY, - CardBrowserColumn.FSRS_STABILITY -> null + CardBrowserColumn.FSRS_STABILITY, + -> null } - } - private fun getEaseForCards(): String { - return if (card.type == Consts.CARD_TYPE_NEW) { + private fun getEaseForCards(): String = + if (card.type == Consts.CARD_TYPE_NEW) { AnkiDroidApp.instance.getString(R.string.card_browser_interval_new_card) } else { "${card.factor / 10}%" } - } private fun getAvgEaseForNotes(): String { val avgEase = NoteService.avgEase(col, card.note(col)) @@ -2233,13 +2325,12 @@ open class CardBrowser : } } - private fun queryIntervalForCards(): String { - return when (card.type) { + private fun queryIntervalForCards(): String = + when (card.type) { Consts.CARD_TYPE_NEW -> AnkiDroidApp.instance.getString(R.string.card_browser_interval_new_card) Consts.CARD_TYPE_LRN -> AnkiDroidApp.instance.getString(R.string.card_browser_interval_learning_card) else -> roundedTimeSpanUnformatted(AnkiDroidApp.instance, card.ivl * SECONDS_PER_DAY) } - } private fun queryAvgIntervalForNotes(): String { val avgInterval = card.avgIntervalOfNote(col) @@ -2253,7 +2344,11 @@ open class CardBrowser : /** pre compute the note and question/answer. It can safely * be called twice without doing extra work. */ - fun load(reload: Boolean, column1: CardBrowserColumn, column2: CardBrowserColumn) { + fun load( + reload: Boolean, + column1: CardBrowserColumn, + column2: CardBrowserColumn, + ) { if (reload) { reload() } @@ -2278,11 +2373,12 @@ open class CardBrowser : val qa = card.renderOutput(col, reload = true, browser = true) // Render full question / answer if the bafmt (i.e. "browser appearance") setting forced blank result if (qa.questionText.isEmpty() || qa.answerText.isEmpty()) { - val (questionText, answerText) = card.renderOutput( - col, - reload = true, - browser = false - ) + val (questionText, answerText) = + card.renderOutput( + col, + reload = true, + browser = false, + ) if (qa.questionText.isEmpty()) { qa.questionText = questionText } @@ -2316,9 +2412,10 @@ open class CardBrowser : } } - override fun hashCode(): Int { - return java.lang.Long.valueOf(id).hashCode() - } + override fun hashCode(): Int = + java.lang.Long + .valueOf(id) + .hashCode() } /** @@ -2367,16 +2464,19 @@ open class CardBrowser : withProgress { viewModel.launchSearchForCards(searchQuery)?.join() } } - override fun opExecuted(changes: OpChanges, handler: Any?) { + override fun opExecuted( + changes: OpChanges, + handler: Any?, + ) { if (handler === this || handler === viewModel) { return } if (( - changes.browserSidebar || - changes.browserTable || - changes.noteText || - changes.card + changes.browserSidebar || + changes.browserTable || + changes.noteText || + changes.card ) ) { refreshAfterUndo() @@ -2384,40 +2484,41 @@ open class CardBrowser : } override val shortcuts - get() = ShortcutGroup( - listOf( - shortcut("Ctrl+Shift+A", R.string.edit_tags_dialog), - shortcut("Ctrl+A", R.string.card_browser_select_all), - shortcut("Ctrl+Shift+E", Translations::exportingExport), - shortcut("Ctrl+E", R.string.menu_add_note), - shortcut("E", R.string.cardeditor_title_edit_card), - shortcut("Ctrl+D", R.string.card_browser_change_deck), - shortcut("Ctrl+K", Translations::browsingToggleMark), - shortcut("Ctrl+Alt+R", Translations::browsingReschedule), - shortcut("DEL", R.string.delete_card_title), - shortcut("Ctrl+Alt+N", R.string.reset_card_dialog_title), - shortcut("Ctrl+Alt+T", R.string.toggle_cards_notes), - shortcut("Ctrl+T", R.string.card_browser_search_by_tag), - shortcut("Ctrl+Shift+S", Translations::actionsReposition), - shortcut("Ctrl+Alt+S", R.string.card_browser_list_my_searches), - shortcut("Ctrl+S", R.string.card_browser_list_my_searches_save), - shortcut("Alt+S", R.string.card_browser_show_suspended), - shortcut("Ctrl+Shift+J", Translations::browsingToggleBury), - shortcut("Ctrl+J", Translations::browsingToggleSuspend), - shortcut("Ctrl+Shift+I", Translations::actionsCardInfo), - shortcut("Ctrl+O", R.string.show_order_dialog), - shortcut("Ctrl+M", R.string.card_browser_show_marked), - shortcut("Esc", R.string.card_browser_select_none), - shortcut("Ctrl+1", R.string.gesture_flag_red), - shortcut("Ctrl+2", R.string.gesture_flag_orange), - shortcut("Ctrl+3", R.string.gesture_flag_green), - shortcut("Ctrl+4", R.string.gesture_flag_blue), - shortcut("Ctrl+5", R.string.gesture_flag_pink), - shortcut("Ctrl+6", R.string.gesture_flag_turquoise), - shortcut("Ctrl+7", R.string.gesture_flag_purple) - ), - R.string.card_browser_context_menu - ) + get() = + ShortcutGroup( + listOf( + shortcut("Ctrl+Shift+A", R.string.edit_tags_dialog), + shortcut("Ctrl+A", R.string.card_browser_select_all), + shortcut("Ctrl+Shift+E", Translations::exportingExport), + shortcut("Ctrl+E", R.string.menu_add_note), + shortcut("E", R.string.cardeditor_title_edit_card), + shortcut("Ctrl+D", R.string.card_browser_change_deck), + shortcut("Ctrl+K", Translations::browsingToggleMark), + shortcut("Ctrl+Alt+R", Translations::browsingReschedule), + shortcut("DEL", R.string.delete_card_title), + shortcut("Ctrl+Alt+N", R.string.reset_card_dialog_title), + shortcut("Ctrl+Alt+T", R.string.toggle_cards_notes), + shortcut("Ctrl+T", R.string.card_browser_search_by_tag), + shortcut("Ctrl+Shift+S", Translations::actionsReposition), + shortcut("Ctrl+Alt+S", R.string.card_browser_list_my_searches), + shortcut("Ctrl+S", R.string.card_browser_list_my_searches_save), + shortcut("Alt+S", R.string.card_browser_show_suspended), + shortcut("Ctrl+Shift+J", Translations::browsingToggleBury), + shortcut("Ctrl+J", Translations::browsingToggleSuspend), + shortcut("Ctrl+Shift+I", Translations::actionsCardInfo), + shortcut("Ctrl+O", R.string.show_order_dialog), + shortcut("Ctrl+M", R.string.card_browser_show_marked), + shortcut("Esc", R.string.card_browser_select_none), + shortcut("Ctrl+1", R.string.gesture_flag_red), + shortcut("Ctrl+2", R.string.gesture_flag_orange), + shortcut("Ctrl+3", R.string.gesture_flag_green), + shortcut("Ctrl+4", R.string.gesture_flag_blue), + shortcut("Ctrl+5", R.string.gesture_flag_pink), + shortcut("Ctrl+6", R.string.gesture_flag_turquoise), + shortcut("Ctrl+7", R.string.gesture_flag_purple), + ), + R.string.card_browser_context_menu, + ) companion object { /** @@ -2437,15 +2538,16 @@ open class CardBrowser : fun clearLastDeckId() = SharedPreferencesLastDeckIdRepository.clearLastDeckId() @VisibleForTesting - fun createAddNoteIntent(context: Context, viewModel: CardBrowserViewModel): Intent { - return NoteEditorLauncher.AddNoteFromCardBrowser(viewModel).getIntent(context) - } + fun createAddNoteIntent( + context: Context, + viewModel: CardBrowserViewModel, + ): Intent = NoteEditorLauncher.AddNoteFromCardBrowser(viewModel).getIntent(context) @CheckResult private fun formatQA( text: String, qa: TemplateManager.TemplateRenderContext.TemplateRenderOutput, - context: Context + context: Context, ): String { val showFilenames = context.sharedPrefs().getBoolean("card_browser_show_media_filenames", false) @@ -2462,9 +2564,9 @@ open class CardBrowser : fun formatQAInternal( txt: String, qa: TemplateManager.TemplateRenderContext.TemplateRenderOutput, - showFileNames: Boolean + showFileNames: Boolean, ): String { - /* Strips all formatting from the string txt for use in displaying question/answer in browser */ + // Strips all formatting from the string txt for use in displaying question/answer in browser var s = txt s = s.replace("".toRegex(), "") s = s.replace("
", " ") @@ -2480,7 +2582,10 @@ open class CardBrowser : return s } - fun dueString(col: Collection, card: Card): String { + fun dueString( + col: Collection, + card: Card, + ): String { var t = nextDue(col, card) if (card.queue < 0) { t = "($t)" @@ -2489,25 +2594,30 @@ open class CardBrowser : } @VisibleForTesting - fun nextDue(col: Collection, card: Card): String { + fun nextDue( + col: Collection, + card: Card, + ): String { val date: Long val due = card.due - date = if (card.isInDynamicDeck) { - return AnkiDroidApp.appResources.getString(R.string.card_browser_due_filtered_card) - } else if (card.queue == Consts.QUEUE_TYPE_LRN) { - due.toLong() - } else if (card.queue == Consts.QUEUE_TYPE_NEW || card.type == Consts.CARD_TYPE_NEW) { - return due.toString() - } else if (card.queue == Consts.QUEUE_TYPE_REV || - card.queue == Consts.QUEUE_TYPE_DAY_LEARN_RELEARN || - card.type == Consts.CARD_TYPE_REV && card.queue < 0 - ) { - val time = TimeManager.time.intTime() - val nbDaySinceCreation = due - col.sched.today - time + nbDaySinceCreation * SECONDS_PER_DAY - } else { - return "" - } + date = + if (card.isInDynamicDeck) { + return AnkiDroidApp.appResources.getString(R.string.card_browser_due_filtered_card) + } else if (card.queue == Consts.QUEUE_TYPE_LRN) { + due.toLong() + } else if (card.queue == Consts.QUEUE_TYPE_NEW || card.type == Consts.CARD_TYPE_NEW) { + return due.toString() + } else if (card.queue == Consts.QUEUE_TYPE_REV || + card.queue == Consts.QUEUE_TYPE_DAY_LEARN_RELEARN || + card.type == Consts.CARD_TYPE_REV && + card.queue < 0 + ) { + val time = TimeManager.time.intTime() + val nbDaySinceCreation = due - col.sched.today + time + nbDaySinceCreation * SECONDS_PER_DAY + } else { + return "" + } return LanguageUtil.getShortDateFormatFromS(date) } // In Anki Desktop, a card with oDue <> 0 && oDid == 0 is not marked as dynamic. } @@ -2531,21 +2641,24 @@ open class CardBrowser : suspend fun searchForCards( query: String, order: SortOrder, - cardsOrNotes: CardsOrNotes -): MutableList { - return withCol { - (if (cardsOrNotes == CARDS) findCards(query, order) else findOneCardByNote(query, order)).asSequence() + cardsOrNotes: CardsOrNotes, +): MutableList = + withCol { + (if (cardsOrNotes == CARDS) findCards(query, order) else findOneCardByNote(query, order)) + .asSequence() .toCardCache(this@withCol, cardsOrNotes) .toMutableList() } -} -private fun Sequence.toCardCache(col: Collection, isInCardMode: CardsOrNotes): Sequence { - return this.mapIndexed { idx, cid -> CardBrowser.CardCache(cid, col, idx, isInCardMode) } -} +private fun Sequence.toCardCache( + col: Collection, + isInCardMode: CardsOrNotes, +): Sequence = this.mapIndexed { idx, cid -> CardBrowser.CardCache(cid, col, idx, isInCardMode) } -class PreviewerDestination(val currentIndex: Int, val previewerIdsFile: PreviewerIdsFile) +class PreviewerDestination( + val currentIndex: Int, + val previewerIdsFile: PreviewerIdsFile, +) @CheckResult -fun PreviewerDestination.toIntent(context: Context) = - PreviewerFragment.getIntent(context, previewerIdsFile, currentIndex) +fun PreviewerDestination.toIntent(context: Context) = PreviewerFragment.getIntent(context, previewerIdsFile, currentIndex) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateBrowserAppearanceEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateBrowserAppearanceEditor.kt index d566d79d0e39..900ccc21fa9c 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 questionEditText: EditText private lateinit var answerEditText: EditText + override fun onCreate(savedInstanceState: Bundle?) { if (showedActivityFailedScreen(savedInstanceState)) { return @@ -133,22 +134,16 @@ class CardTemplateBrowserAppearanceEditor : AnkiActivity() { setTitle(R.string.card_template_browser_appearance_title) } - private fun answerHasChanged(intent: Intent): Boolean { - return intent.getStringExtra(INTENT_ANSWER_FORMAT) != answerFormat - } + private fun answerHasChanged(intent: Intent): Boolean = intent.getStringExtra(INTENT_ANSWER_FORMAT) != answerFormat - private fun questionHasChanged(intent: Intent): Boolean { - return intent.getStringExtra(INTENT_QUESTION_FORMAT) != questionFormat - } + private fun questionHasChanged(intent: Intent): Boolean = intent.getStringExtra(INTENT_QUESTION_FORMAT) != questionFormat private val questionFormat: String get() = getTextValue(questionEditText) private val answerFormat: String get() = getTextValue(answerEditText) - private fun getTextValue(editText: EditText): String { - return editText.text.toString() - } + private fun getTextValue(editText: EditText): String = editText.text.toString() private fun restoreDefaultAndClose() { Timber.i("Restoring Default and Closing") @@ -165,24 +160,27 @@ 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() } - private fun hasChanges(): Boolean { - return try { + private fun hasChanges(): Boolean = + try { questionHasChanged(intent) || answerHasChanged(intent) } catch (e: Exception) { Timber.w(e, "Failed to detect changes. Assuming true") true } - } - class Result private constructor(question: String?, answer: String?) { + class Result private constructor( + question: String?, + answer: String?, + ) { val question: String = question ?: VALUE_USE_DEFAULT val answer: String = answer ?: VALUE_USE_DEFAULT @@ -193,8 +191,8 @@ class CardTemplateBrowserAppearanceEditor : AnkiActivity() { companion object { @Contract("null -> null") - fun fromIntent(intent: Intent?): Result? { - return if (intent == null) { + fun fromIntent(intent: Intent?): Result? = + if (intent == null) { null } else { try { @@ -206,7 +204,6 @@ class CardTemplateBrowserAppearanceEditor : AnkiActivity() { null } } - } } } @@ -218,18 +215,24 @@ 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 { - return Intent(context, CardTemplateBrowserAppearanceEditor::class.java).apply { + fun getIntent( + context: Context, + questionFormat: String, + answerFormat: String, + ): Intent = + 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 31132e2e8626..9730bbcb3751 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt @@ -103,7 +103,9 @@ import kotlin.time.Duration.Companion.seconds * Allows the user to view the template for the current note type */ @KotlinCleanup("lateinit wherever possible") -open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { +open class CardTemplateEditor : + AnkiActivity(), + DeckSelectionListener { @VisibleForTesting lateinit var viewPager: ViewPager2 private var slidingTabLayout: TabLayout? = null @@ -199,14 +201,15 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { val notetypeFile = NotetypeFile(this@CardTemplateEditor, notetype) val ord = viewPager.currentItem val note = withCol { currentFragment?.getNote(this) ?: Note.fromNotetypeId(this@withCol, notetype.id) } - val args = TemplatePreviewerArguments( - notetypeFile = notetypeFile, - id = note.id, - ord = ord, - fields = note.fields, - tags = note.tags, - fillEmpty = true - ) + val args = + TemplatePreviewerArguments( + notetypeFile = notetypeFile, + id = note.id, + ord = ord, + fields = note.fields, + tags = note.tags, + fillEmpty = true, + ) val backgroundColor = Themes.getColorFromAttr(this@CardTemplateEditor, R.attr.alternativeBackgroundColor) val fragment = TemplatePreviewerFragment.newInstance(args, backgroundColor) supportFragmentManager.commitNow { @@ -288,13 +291,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?) { @@ -314,15 +318,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) @@ -330,9 +335,14 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { invalidateOptionsMenu() } - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { val currentFragment = currentFragment ?: return super.onKeyUp(keyCode, event) - if (!event.isCtrlPressed) { return super.onKeyUp(keyCode, event) } + if (!event.isCtrlPressed) { + return super.onKeyUp(keyCode, event) + } when (keyCode) { KeyEvent.KEYCODE_P -> { Timber.i("Ctrl+P: Perform preview from keypress") @@ -392,21 +402,24 @@ 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) { - + inner class TemplatePagerAdapter( + fragmentActivity: FragmentActivity, + ) : FragmentStateAdapter(fragmentActivity) { private var baseId: Long = 0 override fun createFragment(position: Int): Fragment { @@ -417,9 +430,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { override fun getItemCount(): Int = tempModel?.templateCount ?: 0 - override fun getItemId(position: Int): Long { - return baseId + position - } + override fun getItemId(position: Int): Long = baseId + position override fun containsItem(id: Long): Boolean { @Suppress("ConvertTwoComparisonsToRangeCheck") // more readable without the range check @@ -433,23 +444,24 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } override val shortcuts - get() = ShortcutGroup( - listOf( - shortcut("Ctrl+P", R.string.card_editor_preview_card), - shortcut("Ctrl+1", R.string.edit_front_template), - shortcut("Ctrl+2", R.string.edit_back_template), - shortcut("Ctrl+3", R.string.edit_styling), - shortcut("Ctrl+S", R.string.save), - shortcut("Ctrl+I", R.string.card_template_editor_insert_field), - shortcut("Ctrl+A", Translations::cardTemplatesAddCardType), - shortcut("Ctrl+R", Translations::cardTemplatesRenameCardType), - shortcut("Ctrl+B", R.string.edit_browser_appearance), - shortcut("Ctrl+D", Translations::cardTemplatesRemoveCardType), - shortcut("Ctrl+O", Translations::cardTemplatesDeckOverride), - shortcut("Ctrl+M", R.string.copy_the_template) - ), - R.string.card_template_editor_group - ) + get() = + ShortcutGroup( + listOf( + shortcut("Ctrl+P", R.string.card_editor_preview_card), + shortcut("Ctrl+1", R.string.edit_front_template), + shortcut("Ctrl+2", R.string.edit_back_template), + shortcut("Ctrl+3", R.string.edit_styling), + shortcut("Ctrl+S", R.string.save), + shortcut("Ctrl+I", R.string.card_template_editor_insert_field), + shortcut("Ctrl+A", Translations::cardTemplatesAddCardType), + shortcut("Ctrl+R", Translations::cardTemplatesRenameCardType), + shortcut("Ctrl+B", R.string.edit_browser_appearance), + shortcut("Ctrl+D", Translations::cardTemplatesRemoveCardType), + shortcut("Ctrl+O", Translations::cardTemplatesDeckOverride), + shortcut("Ctrl+M", R.string.copy_the_template), + ), + R.string.card_template_editor_group, + ) class CardTemplateFragment : Fragment() { private val refreshFragmentHandler = Handler(Looper.getMainLooper()) @@ -463,19 +475,24 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { lateinit var tempModel: CardTemplateNotetype lateinit var bottomNavigation: BottomNavigationView - 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 templateEditor = activity as CardTemplateEditor val mainView = inflater.inflate(R.layout.card_template_editor_item, container, false) val cardIndex = requireArguments().getInt(CARD_INDEX) tempModel = templateEditor.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 + } currentEditorTitle = mainView.findViewById(R.id.title_edit) editorEditText = mainView.findViewById(R.id.editor_editText) @@ -489,7 +506,12 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { templateEditor.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 @@ -501,37 +523,50 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { templateEditor.tabToViewId[cardIndex] ?: requireArguments().getInt(EDITOR_VIEW_ID_KEY) // Set text change listeners - val templateEditorWatcher: TextWatcher = object : TextWatcher { - /** - * Declare a nullable variable refreshFragmentRunnable of type Runnable. - * This will hold a reference to the Runnable that refreshes the previewer fragment. - * It is used to manage delayed fragment updates and can be null if no updates in card. - */ - private var refreshFragmentRunnable: Runnable? = null - override fun afterTextChanged(arg0: Editable) { - refreshFragmentRunnable?.let { refreshFragmentHandler.removeCallbacks(it) } - templateEditor.tabToCursorPosition[cardIndex] = editorEditText.selectionStart - when (currentEditorViewId) { - R.id.styling_edit -> tempModel.updateCss(editorEditText.text.toString()) - R.id.back_edit -> template.put("afmt", editorEditText.text) - else -> template.put("qfmt", editorEditText.text) + val templateEditorWatcher: TextWatcher = + object : TextWatcher { + /** + * Declare a nullable variable refreshFragmentRunnable of type Runnable. + * This will hold a reference to the Runnable that refreshes the previewer fragment. + * It is used to manage delayed fragment updates and can be null if no updates in card. + */ + private var refreshFragmentRunnable: Runnable? = null + + override fun afterTextChanged(arg0: Editable) { + refreshFragmentRunnable?.let { refreshFragmentHandler.removeCallbacks(it) } + templateEditor.tabToCursorPosition[cardIndex] = editorEditText.selectionStart + when (currentEditorViewId) { + R.id.styling_edit -> tempModel.updateCss(editorEditText.text.toString()) + R.id.back_edit -> template.put("afmt", editorEditText.text) + else -> template.put("qfmt", editorEditText.text) + } + templateEditor.tempModel!!.updateTemplate(cardIndex, template) + val updateRunnable = + Runnable { + templateEditor.loadTemplatePreviewerFragmentIfFragmented() + } + refreshFragmentRunnable = updateRunnable + refreshFragmentHandler.postDelayed(updateRunnable, REFRESH_PREVIEW_DELAY) } - templateEditor.tempModel!!.updateTemplate(cardIndex, template) - val updateRunnable = Runnable { - templateEditor.loadTemplatePreviewerFragmentIfFragmented() - } - refreshFragmentRunnable = updateRunnable - refreshFragmentHandler.postDelayed(updateRunnable, REFRESH_PREVIEW_DELAY) - } - 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 + } } - } editorEditText.addTextChangedListener(templateEditorWatcher) /* When keyboard is visible, hide the bottom navigation bar to allow viewing @@ -556,11 +591,15 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { private inner class ActionModeCallback : ActionMode.Callback { private val insertFieldId = 1 - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - return true - } + override fun onCreateActionMode( + mode: ActionMode, + menu: Menu, + ): Boolean = true - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + override fun onPrepareActionMode( + mode: ActionMode, + menu: Menu, + ): Boolean { if (menu.findItem(insertFieldId) != null) { return false } @@ -574,7 +613,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 (itemId == insertFieldId) { showInsertFieldDialog() @@ -591,7 +633,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", ) fun showInsertFieldDialog() { templateEditor.fieldNames?.let { fieldNames -> @@ -613,7 +655,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { RenameCardTemplateDialog.showInstance( requireContext(), - prefill = template.getString("name") + prefill = template.getString("name"), ) { newName -> template.put("name", newName) Timber.i("updated card template name") @@ -643,7 +685,11 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { editorEditText.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 editorEditText.setText(editorContent) currentEditorTitle!!.text = resources.getString(editorTitleId) @@ -651,16 +697,23 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { editorEditText.requestFocus() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - templateEditor.slidingTabLayout?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(p0: TabLayout.Tab?) { - templateEditor.loadTemplatePreviewerFragmentIfFragmented() - } - override fun onTabUnselected(p0: TabLayout.Tab?) { - } - override fun onTabReselected(p0: TabLayout.Tab?) { - } - }) + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + templateEditor.slidingTabLayout?.addOnTabSelectedListener( + object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(p0: TabLayout.Tab?) { + templateEditor.loadTemplatePreviewerFragmentIfFragmented() + } + + override fun onTabUnselected(p0: TabLayout.Tab?) { + } + + override fun onTabReselected(p0: TabLayout.Tab?) { + } + }, + ) parentFragmentManager.setFragmentResultListener(REQUEST_FIELD_INSERT, viewLifecycleOwner) { key, bundle -> if (key == REQUEST_FIELD_INSERT) { // this is guaranteed to be non null, as we put a non null value on the other side @@ -685,17 +738,18 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { private fun setupMenu() { (requireActivity() as MenuHost).addMenuProvider( object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + override fun onCreateMenu( + menu: Menu, + menuInflater: MenuInflater, + ) { menuInflater.inflate(R.menu.card_template_editor, menu) setupCommonMenu(menu) } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - return handleCommonMenuItemSelected(menuItem) - } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = handleCommonMenuItemSelected(menuItem) }, viewLifecycleOwner, - Lifecycle.State.RESUMED + Lifecycle.State.RESUMED, ) } @@ -717,23 +771,26 @@ 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) } /* showOrphanNoteDialog shows a AlertDialog if the deletionWouldOrphanNote returns true - * it displays a warning for the user when they attempt to delete a card type that + * it displays a warning for the user when they attempt to delete a card type that would leave some notes without any cards (orphan notes) */ private fun showOrphanNoteDialog() { - val builder = AlertDialog.Builder(requireContext()) - .setTitle(R.string.orphan_note_title) - .setMessage(R.string.orphan_note_message) - .setPositiveButton(android.R.string.ok, null) + val builder = + AlertDialog + .Builder(requireContext()) + .setTitle(R.string.orphan_note_title) + .setMessage(R.string.orphan_note_message) + .setPositiveButton(android.R.string.ok, null) builder.show() } @@ -749,11 +806,12 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { val ordinal = templateEditor.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(templateEditor.tempModel!!, ordinal)) { - templateEditor.getColUnsafe.notetypes.tmplUseCount(templateEditor.tempModel!!.notetype, ordinal) - } else { - 0 - } + val numAffectedCards = + if (!CardTemplateNotetype.isOrdinalPendingAdd(templateEditor.tempModel!!, ordinal)) { + templateEditor.getColUnsafe.notetypes.tmplUseCount(templateEditor.tempModel!!.notetype, ordinal) + } else { + 0 + } confirmAddCards(templateEditor.tempModel!!.notetype, numAffectedCards) } @@ -792,11 +850,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) } @@ -876,18 +935,19 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } private val currentTemplate: CardTemplate? - get() = try { - val tempModel = templateEditor.tempModel - val template: JSONObject = tempModel!!.getTemplate(templateEditor.viewPager.currentItem) - CardTemplate( - front = template.getString("qfmt"), - back = template.getString("afmt"), - style = tempModel.css - ) - } catch (e: Exception) { - Timber.w(e, "Exception loading template in CardTemplateFragment. Probably stale fragment.") - null - } + get() = + try { + val tempModel = templateEditor.tempModel + val template: JSONObject = tempModel!!.getTemplate(templateEditor.viewPager.currentItem) + CardTemplate( + front = template.getString("qfmt"), + back = template.getString("afmt"), + style = tempModel.css, + ) + } catch (e: Exception) { + Timber.w(e, "Exception loading template in CardTemplateFragment. Probably stale fragment.") + null + } /** Copies the template to clipboard in markdown format */ fun copyMarkdownTemplateToClipboard() { @@ -897,7 +957,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { context?.let { ctx -> ctx.copyToClipboard( - template.toMarkdown(ctx) + template.toMarkdown(ctx), ) } } @@ -923,37 +983,39 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { val notetypeFile = NotetypeFile(requireContext(), notetype) val ord = templateEditor.viewPager.currentItem val note = withCol { getNote(this) ?: Note.fromNotetypeId(this@withCol, notetype.id) } - val args = TemplatePreviewerArguments( - notetypeFile = notetypeFile, - id = note.id, - ord = ord, - fields = note.fields, - tags = note.tags, - fillEmpty = true - ) + val args = + TemplatePreviewerArguments( + notetypeFile = notetypeFile, + id = note.id, + ord = ord, + fields = note.fields, + tags = note.tags, + fillEmpty = true, + ) val intent = TemplatePreviewerPage.getIntent(requireContext(), args) startActivity(intent) } } - fun displayDeckOverrideDialog(tempModel: CardTemplateNotetype) = launchCatchingTask { - val activity = requireActivity() as AnkiActivity - if (tempModel.notetype.isCloze) { - showSnackbar(getString(R.string.multimedia_editor_something_wrong), Snackbar.LENGTH_SHORT) - return@launchCatchingTask + fun displayDeckOverrideDialog(tempModel: CardTemplateNotetype) = + launchCatchingTask { + val activity = requireActivity() as AnkiActivity + if (tempModel.notetype.isCloze) { + showSnackbar(getString(R.string.multimedia_editor_something_wrong), Snackbar.LENGTH_SHORT) + return@launchCatchingTask + } + val name = getCurrentTemplateName(tempModel) + val explanation = getString(R.string.deck_override_explanation, name) + // Anki Desktop allows Dynamic decks, have reported this as a bug: + // https://forums.ankiweb.net/t/minor-bug-deck-override-to-filtered-deck/1493 + val decks = SelectableDeck.fromCollection(includeFiltered = false) + val title = getString(R.string.card_template_editor_deck_override) + val dialog = DeckSelectionDialog.newInstance(title, explanation, true, decks) + activity.showDialogFragment(dialog) } - val name = getCurrentTemplateName(tempModel) - val explanation = getString(R.string.deck_override_explanation, name) - // Anki Desktop allows Dynamic decks, have reported this as a bug: - // https://forums.ankiweb.net/t/minor-bug-deck-override-to-filtered-deck/1493 - val decks = SelectableDeck.fromCollection(includeFiltered = false) - val title = getString(R.string.card_template_editor_deck_override) - val dialog = DeckSelectionDialog.newInstance(title, explanation, true, decks) - activity.showDialogFragment(dialog) - } - private fun getCurrentTemplateName(tempModel: CardTemplateNotetype): String { - return try { + private fun getCurrentTemplateName(tempModel: CardTemplateNotetype): String = + try { val ordinal = templateEditor.viewPager.currentItem val template = tempModel.getTemplate(ordinal) template.getString("name") @@ -961,7 +1023,6 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { Timber.w(e, "Failed to get name for template") "" } - } private fun launchCardBrowserAppearance(currentTemplate: JSONObject) { val context = AnkiDroidApp.instance.baseContext @@ -973,7 +1034,9 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { private fun getCurrentTemplate(): JSONObject? { val currentCardTemplateIndex = getCurrentCardTemplateIndex() return try { - templateEditor.tempModel!!.notetype.getJSONArray("tmpls") + templateEditor.tempModel!! + .notetype + .getJSONArray("tmpls") .getJSONObject(currentCardTemplateIndex) } catch (e: JSONException) { Timber.w(e, "CardTemplateEditor::getCurrentTemplate - unexpectedly unable to fetch template? %d", currentCardTemplateIndex) @@ -989,7 +1052,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. @@ -1040,9 +1107,7 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } } - private fun modelHasChanged(): Boolean { - return templateEditor.modelHasChanged() - } + private fun modelHasChanged(): Boolean = templateEditor.modelHasChanged() /** * Confirm if the user wants to delete all the cards associated with current template @@ -1051,16 +1116,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) } @@ -1074,15 +1144,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) } @@ -1112,11 +1186,12 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { e.log() val d = ConfirmationDialog() d.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = Runnable { - templateEditor.getColUnsafe.modSchemaNoCheck() - schemaChangingAction.run() - templateEditor.dismissAllDialogFragments() - } + val confirm = + Runnable { + templateEditor.getColUnsafe.modSchemaNoCheck() + schemaChangingAction.run() + templateEditor.dismissAllDialogFragments() + } val cancel = Runnable { templateEditor.dismissAllDialogFragments() } d.setConfirm(confirm) d.setCancel(cancel) @@ -1128,7 +1203,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()) { @@ -1210,7 +1288,11 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { } } - data class CardTemplate(val front: String, val back: String, val style: String) { + data class CardTemplate( + val front: String, + val back: String, + val style: String, + ) { fun toMarkdown(context: Context) = // backticks are not supported by old reddit buildString { @@ -1228,7 +1310,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 743fb5e880e0..2e18e4d67622 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateNotetype.kt @@ -38,18 +38,22 @@ import java.io.IOException /** A wrapper for a notetype in JSON format with helpers for editing the notetype. */ @KotlinCleanup("_templateChanges -> use templateChanges") -class CardTemplateNotetype(val notetype: NotetypeJson) { +class CardTemplateNotetype( + val notetype: NotetypeJson, +) { enum class ChangeType { - ADD, DELETE + ADD, + DELETE, } private var _templateChanges = ArrayList>() var editedModelFileName: String? = null - fun toBundle(): Bundle = bundleOf( - INTENT_MODEL_FILENAME to saveTempModel(AnkiDroidApp.instance.applicationContext, notetype), - "mTemplateChanges" to _templateChanges - ) + fun toBundle(): Bundle = + bundleOf( + INTENT_MODEL_FILENAME to saveTempModel(AnkiDroidApp.instance.applicationContext, notetype), + "mTemplateChanges" to _templateChanges, + ) private fun loadTemplateChanges(bundle: Bundle) { try { @@ -77,7 +81,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) } @@ -102,7 +109,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) @@ -113,16 +123,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 + } } } } @@ -196,7 +208,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { "dumpChanges() During save change %s will be ord/type %s/%s", i, adjustedChange[0], - adjustedChange[1] + adjustedChange[1], ) } } @@ -231,7 +243,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { "getAdjustedTemplateChanges() change %s ordinal adjusted from %s to %s", i, change[0], - adjustedChange[0] + adjustedChange[0], ) } ChangeType.DELETE -> {} @@ -248,7 +260,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 @@ -277,7 +289,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { ordinalAdjustment++ Timber.d( "compactTemplateChanges() delete affecting purged template, shifting basis, adj: %s", - ordinalAdjustment + ordinalAdjustment, ) } @@ -307,12 +319,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 + } return CardTemplateNotetype(tempNotetypeJSON).apply { loadTemplateChanges(bundle) } @@ -322,7 +335,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 { @@ -378,7 +394,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] @@ -387,7 +406,7 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { Timber.d( "isOrdinalPendingAdd() found ord %s was pending add (would adjust to %s)", ord, - adjustedOrdinal + adjustedOrdinal, ) return true } @@ -402,7 +421,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 } @@ -422,24 +444,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 } @@ -456,8 +479,10 @@ class CardTemplateNotetype(val notetype: NotetypeJson) { * [limit of 1MB](https://developer.android.com/reference/android/os/TransactionTooLargeException.html) * for [Bundle] transactions, and notetypes can be bigger than that (#5600). */ -class NotetypeFile(path: String) : File(path), Parcelable { - +class NotetypeFile( + path: String, +) : File(path), + Parcelable { /** * @param directory where the file will be saved * @param notetype to be stored @@ -478,8 +503,8 @@ class NotetypeFile(path: String) : File(path), Parcelable { */ constructor(context: Context, notetype: NotetypeJson) : this(context.cacheDir, notetype) - fun getNotetype(): NotetypeJson { - return try { + fun getNotetype(): NotetypeJson = + try { ByteArrayOutputStream().use { target -> compat.copyFile(absolutePath, target) NotetypeJson(target.toString()) @@ -488,25 +513,24 @@ class NotetypeFile(path: String) : File(path), Parcelable { Timber.e(e, "Unable to read+parse tempModel from file %s", absolutePath) throw e } - } override fun describeContents(): Int = 0 - override fun writeToParcel(dest: Parcel, flags: Int) { + override fun writeToParcel( + dest: Parcel, + flags: Int, + ) { dest.writeString(path) } companion object { @JvmField @Suppress("unused") - val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel?): NotetypeFile { - return NotetypeFile(source!!.readString()!!) - } + val CREATOR = + object : Parcelable.Creator { + override fun createFromParcel(source: Parcel?): NotetypeFile = NotetypeFile(source!!.readString()!!) - override fun newArray(size: Int): Array { - return arrayOf() + override fun newArray(size: Int): Array = arrayOf() } - } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardUtils.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardUtils.kt index 8622b94cd7ac..3f6103b9261e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardUtils.kt @@ -13,7 +13,10 @@ object CardUtils { /** * @return List of corresponding notes without duplicates, even if the input list has multiple cards of the same note. */ - fun getNotes(col: Collection, cards: kotlin.collections.Collection): Set { + fun getNotes( + col: Collection, + cards: kotlin.collections.Collection, + ): Set { val notes: MutableSet = hashSetInit(cards.size) for (card in cards) { notes.add(card.note(col)) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt index 508e4e5d2248..c41c7f898dfd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt @@ -53,15 +53,14 @@ object CollectionHelper { */ const val PREF_COLLECTION_PATH = "deckPath" - fun getCollectionSize(context: Context): Long? { - return try { + fun getCollectionSize(context: Context): Long? = + try { val path = getCollectionPath(context) File(path).length() } catch (e: Exception) { Timber.e(e, "Error getting collection Length") null } - } /** * Create the AnkiDroid directory if it doesn't exist and add a .nomedia file to it if needed. @@ -105,15 +104,14 @@ object CollectionHelper { * @return whether or not dir is accessible * @param context to get directory with */ - fun isCurrentAnkiDroidDirAccessible(context: Context): Boolean { - return try { + fun isCurrentAnkiDroidDirAccessible(context: Context): Boolean = + try { initializeAnkiDroidDirectory(getCurrentAnkiDroidDirectory(context)) true } catch (e: StorageAccessException) { Timber.w(e) false } - } /** * Get the absolute path to a directory that is suitable to be the default starting location @@ -230,7 +228,7 @@ object CollectionHelper { Timber.e("Attempting to determine collection path, but no valid external storage?") throw IllegalStateException( "getExternalFilesDir unexpectedly returned null. Media state: " + - Environment.getExternalStorageState() + Environment.getExternalStorageState(), ) } return externalFilesDir.absolutePath @@ -240,9 +238,7 @@ object CollectionHelper { * @return Returns an array of [File]s reflecting the directories that AnkiDroid can access without storage permissions * @see android.content.Context.getExternalFilesDirs */ - fun getAppSpecificExternalDirectories(context: Context): List { - return context.getExternalFilesDirs(null)?.filterNotNull() ?: listOf() - } + fun getAppSpecificExternalDirectories(context: Context): List = context.getExternalFilesDirs(null)?.filterNotNull() ?: listOf() /** * Returns the absolute path to the private AnkiDroid directory under the app-specific, internal storage directory. @@ -254,17 +250,13 @@ object CollectionHelper { * @param context Used to get the Internal App-Specific directory for AnkiDroid * @return Returns the absolute path to the App-Specific Internal AnkiDroid Directory */ - fun getAppSpecificInternalAnkiDroidDirectory(context: Context): String { - return context.filesDir.absolutePath - } + fun getAppSpecificInternalAnkiDroidDirectory(context: Context): String = context.filesDir.absolutePath /** * * @return the path to the actual [Collection] file */ - fun getCollectionPath(context: Context): String { - return File(getCurrentAnkiDroidDirectory(context), COLLECTION_FILENAME).absolutePath - } + fun getCollectionPath(context: Context): String = File(getCurrentAnkiDroidDirectory(context), COLLECTION_FILENAME).absolutePath /** A temporary override for [getCurrentAnkiDroidDirectory] */ var ankiDroidDirectoryOverride: String? = null @@ -272,13 +264,10 @@ object CollectionHelper { /** * @return the absolute path to the AnkiDroid directory. */ - fun getCurrentAnkiDroidDirectory(context: Context): String { - return getCurrentAnkiDroidDirectoryOptionalContext(context.sharedPrefs()) { context } - } + fun getCurrentAnkiDroidDirectory(context: Context): String = + getCurrentAnkiDroidDirectoryOptionalContext(context.sharedPrefs()) { context } - fun getMediaDirectory(context: Context): File { - return File(getCurrentAnkiDroidDirectory(context), "collection.media") - } + fun getMediaDirectory(context: Context): File = File(getCurrentAnkiDroidDirectory(context), "collection.media") /** * An accessor which makes [Context] optional in the case that [PREF_COLLECTION_PATH] is set @@ -288,27 +277,30 @@ object 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 { - return if (AnkiDroidApp.INSTRUMENTATION_TESTING) { + internal fun getCurrentAnkiDroidDirectoryOptionalContext( + preferences: SharedPreferences, + context: () -> Context, + ): String = + 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(), ) } } - } /** * Resets the AnkiDroid directory to the [getDefaultAnkiDroidDirectory] diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionIntegrityStorageCheck.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionIntegrityStorageCheck.kt index 86be969c9716..7b602a89306b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionIntegrityStorageCheck.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionIntegrityStorageCheck.kt @@ -44,9 +44,7 @@ class CollectionIntegrityStorageCheck { this.errorMessage = errorMessage } - fun shouldWarnOnIntegrityCheck(): Boolean { - return errorMessage != null || fileSystemDoesNotHaveSpaceForBackup() - } + fun shouldWarnOnIntegrityCheck(): Boolean = errorMessage != null || fileSystemDoesNotHaveSpaceForBackup() private fun fileSystemDoesNotHaveSpaceForBackup(): Boolean { // only to be called when mErrorMessage == null @@ -67,29 +65,28 @@ class CollectionIntegrityStorageCheck { val defaultRequiredFreeSpace = defaultRequiredFreeSpace(context) return context.resources.getString( R.string.integrity_check_insufficient_space, - defaultRequiredFreeSpace + defaultRequiredFreeSpace, ) } val required = Formatter.formatShortFileSize(context, requiredSpace) - 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, freeSpace) - 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) - } + private fun fromError(errorMessage: String): CollectionIntegrityStorageCheck = CollectionIntegrityStorageCheck(errorMessage) private fun defaultRequiredFreeSpace(context: Context): String { val oneHundredFiftyMB = @@ -105,8 +102,8 @@ class CollectionIntegrityStorageCheck { return fromError( context.resources.getString( R.string.integrity_check_insufficient_space, - requiredFreeSpace - ) + requiredFreeSpace, + ), ) } @@ -123,8 +120,8 @@ class CollectionIntegrityStorageCheck { return fromError( context.resources.getString( R.string.integrity_check_insufficient_space, - readableFileSize - ) + readableFileSize, + ), ) } return CollectionIntegrityStorageCheck(requiredSpaceInBytes, freeSpace) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt index faa8b0fc13c5..42c7a45d6e61 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt @@ -92,7 +92,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 { if (isRobolectric) { // #16253 Robolectric Windows: `withContext(queue)` is insufficient for serial execution return testMutex.withLock { @@ -113,12 +115,13 @@ 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 { - return withQueue { + suspend fun withCol( + @WorkerThread block: Collection.() -> T, + ): T = + withQueue { ensureOpenInner() block(collection!!) } - } /** * Execute the provided block if the collection is already open. See [withCol] for more. @@ -127,15 +130,16 @@ 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? { - return withQueue { + suspend fun withOpenColOrNull( + @WorkerThread block: Collection.() -> T, + ): T? = + withQueue { if (collection != null && !collection!!.dbClosed) { block(collection!!) } else { null } } - } /** * Return a handle to the backend, creating if necessary. This should only be used @@ -161,7 +165,11 @@ object CollectionManager { return getBackend().tr } - fun compareAnswer(expected: String, given: String, combining: Boolean = true): String { + fun compareAnswer( + expected: String, + given: String, + combining: Boolean = true, + ): String { // bypass the lock, as the type answer code is heavily nested in non-suspend functions return getBackend().compareAnswer(expected, given, combining) } @@ -271,8 +279,8 @@ object CollectionManager { * Note: [runBlocking] inside `RobolectricTest.runTest` will lead to deadlocks, so * under Robolectric, this uses a mutex */ - private fun blockForQueue(block: CollectionManager.() -> T): T { - return if (isRobolectric) { + private fun blockForQueue(block: CollectionManager.() -> T): T = + if (isRobolectric) { testMutex.withLock { block(this) } @@ -281,7 +289,6 @@ object CollectionManager { withQueue(block) } } - } fun closeCollectionBlocking() { runBlocking { ensureClosed() } @@ -293,14 +300,13 @@ object CollectionManager { * the collection while the reference is held. [withCol] * is a better alternative. */ - fun getColUnsafe(): Collection { - return logUIHangs { + fun getColUnsafe(): Collection = + logUIHangs { blockForQueue { ensureOpenInner() collection!! } } - } /** Execute [block]. If it takes more than 100ms of real time, Timber an error like: @@ -317,22 +323,25 @@ 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 - } - } - true - }.first() + 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() Timber.w("blocked main thread for %dms:\n%s", elapsed, caller) } } @@ -341,8 +350,8 @@ object CollectionManager { /** * True if the collection is open. Unsafe, as it has the potential to race. */ - fun isOpenUnsafe(): Boolean { - return logUIHangs { + fun isOpenUnsafe(): Boolean = + logUIHangs { blockForQueue { if (emulatedOpenFailure != null) { false @@ -351,7 +360,6 @@ object CollectionManager { } } } - } /** Use [col] as collection in tests. @@ -411,7 +419,8 @@ object CollectionManager { LOCKED, /** Raises [BackendException.BackendFatalError] */ - FATAL_ERROR + FATAL_ERROR, + ; fun triggerFailure() { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt index 0f3b1658a002..7057f254c39b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt @@ -83,9 +83,9 @@ var throwOnShowError = false fun CoroutineScope.launchCatching( context: CoroutineContext = EmptyCoroutineContext, errorMessageHandler: suspend (String) -> Unit, - block: suspend CoroutineScope.() -> Unit -): Job { - return launch(context) { + block: suspend CoroutineScope.() -> Unit, +): Job = + launch(context) { try { block() } catch (cancellationException: CancellationException) { @@ -100,37 +100,30 @@ fun CoroutineScope.launchCatching( errorMessageHandler.invoke(exception.toString()) } } -} interface OnErrorListener { val onError: MutableSharedFlow } -fun T.launchCatchingIO(block: suspend T.() -> Unit): Job where T : ViewModel, T : OnErrorListener { - return viewModelScope.launchCatching( +fun T.launchCatchingIO(block: suspend T.() -> Unit): Job where T : ViewModel, T : OnErrorListener = + viewModelScope.launchCatching( ioDispatcher, { onError.emit(it) }, - { block() } + { block() }, ) -} fun T.launchCatchingIO( errorMessageHandler: suspend (String) -> Unit, - block: suspend CoroutineScope.() -> Unit -): Job where T : ViewModel { - return viewModelScope.launchCatching( + block: suspend CoroutineScope.() -> Unit, +): Job where T : ViewModel = + viewModelScope.launchCatching( ioDispatcher, - errorMessageHandler + errorMessageHandler, ) { block() } -} -fun CoroutineScope.asyncIO(block: suspend CoroutineScope.() -> T): Deferred { - return async(ioDispatcher, block = block) -} +fun CoroutineScope.asyncIO(block: suspend CoroutineScope.() -> T): Deferred = async(ioDispatcher, block = block) -fun ViewModel.asyncIO(block: suspend CoroutineScope.() -> T): Deferred { - return viewModelScope.asyncIO(block) -} +fun ViewModel.asyncIO(block: suspend CoroutineScope.() -> T): Deferred = viewModelScope.asyncIO(block) /** * Runs a suspend function that catches any uncaught errors and reports them to the user. @@ -144,20 +137,23 @@ fun ViewModel.asyncIO(block: suspend CoroutineScope.() -> T): Deferred { */ suspend fun FragmentActivity.runCatching( errorMessage: String? = null, - block: suspend () -> T? + block: suspend () -> T?, ): T? { // appends the pre-coroutine stack to the error message. Example: // at com.ichi2.anki.CoroutineHelpersKt.launchCatchingTask(CoroutineHelpers.kt:188) // at com.ichi2.anki.CoroutineHelpersKt.launchCatchingTask$default(CoroutineHelpers.kt:184) // at com.ichi2.anki.BackendBackupsKt.performBackupInBackground(BackendBackups.kt:26) // This is only performed in DEBUG mode to reduce performance impact - val callerTrace = if (BuildConfig.DEBUG) { - Thread.currentThread().stackTrace - .drop(14) - .joinToString(prefix = "\tat ", separator = "\n\tat ") - } else { - null - } + val callerTrace = + if (BuildConfig.DEBUG) { + Thread + .currentThread() + .stackTrace + .drop(14) + .joinToString(prefix = "\tat ", separator = "\n\tat ") + } else { + null + } try { return block() @@ -201,24 +197,26 @@ suspend fun FragmentActivity.runCatching( * @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. @@ -227,24 +225,25 @@ fun getCoroutineExceptionHandler(activity: Activity, errorMessage: String? = nul */ fun FragmentActivity.launchCatchingTask( errorMessage: String? = null, - block: suspend CoroutineScope.() -> Unit -): Job { - return lifecycle.coroutineScope.launch { + block: suspend CoroutineScope.() -> Unit, +): Job = + lifecycle.coroutineScope.launch { runCatching(errorMessage) { block() } } -} /** See [FragmentActivity.launchCatchingTask] */ fun Fragment.launchCatchingTask( errorMessage: String? = null, - block: suspend CoroutineScope.() -> Unit -): Job { - return lifecycle.coroutineScope.launch { + block: suspend CoroutineScope.() -> Unit, +): Job = + lifecycle.coroutineScope.launch { requireActivity().runCatching(errorMessage) { block() } } -} -fun showError(context: Context, msg: String) { +fun showError( + context: Context, + msg: String, +) { if (throwOnShowError) throw IllegalStateException("throwOnShowError: $msg") Timber.i("Error dialog displayed") try { @@ -259,7 +258,12 @@ fun showError(context: Context, msg: String) { } } -fun showError(context: Context, msg: String, exception: Throwable, crashReport: Boolean = true) { +fun showError( + context: Context, + msg: String, + exception: Throwable, + crashReport: Boolean = true, +) { if (throwOnShowError) throw IllegalStateException("throwOnShowError: $msg", exception) Timber.i("Error dialog displayed") try { @@ -271,7 +275,7 @@ fun showError(context: Context, msg: String, exception: Throwable, crashReport: setOnDismissListener { CrashReportService.sendExceptionReport( exception, - origin = context::class.java.simpleName + origin = context::class.java.simpleName, ) } } @@ -282,7 +286,7 @@ fun showError(context: Context, msg: String, exception: Throwable, crashReport: if (crashReport) { CrashReportService.sendExceptionReport( exception, - origin = context::class.java.simpleName + origin = context::class.java.simpleName, ) } } @@ -295,19 +299,19 @@ fun showError(context: Context, msg: String, exception: Throwable, crashReport: suspend fun Backend.withProgress( extractProgress: ProgressContext.() -> Unit, updateUi: ProgressContext.() -> Unit, - block: suspend CoroutineScope.() -> T -): T { - return coroutineScope { - val monitor = launch { - monitorProgress(this@withProgress, extractProgress, updateUi) - } + block: suspend CoroutineScope.() -> T, +): T = + coroutineScope { + val monitor = + launch { + monitorProgress(this@withProgress, extractProgress, updateUi) + } try { block() } finally { monitor.cancel() } } -} /** * Run the provided operation, showing a progress window until it completes. @@ -320,21 +324,24 @@ suspend fun FragmentActivity.withProgress( extractProgress: ProgressContext.() -> Unit, onCancel: ((Backend) -> Unit)? = { it.setWantsAbort() }, @StringRes manualCancelButton: Int? = null, - 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 - }, - manualCancelButton = manualCancelButton + onCancel = + if (onCancel != null) { + fun() { + onCancel(backend) + } + } else { + null + }, + manualCancelButton = manualCancelButton, ) { dialog -> backend.withProgress( extractProgress = extractProgress, - updateUi = { updateDialog(dialog) } + updateUi = { updateDialog(dialog) }, ) { op() } @@ -350,27 +357,34 @@ 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 = getString(R.string.dialog_processing), block: suspend () -> T): T = - requireActivity().withProgress(message, block) +suspend fun Fragment.withProgress( + message: String = getString(R.string.dialog_processing), + 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( @@ -378,51 +392,66 @@ suspend fun withProgressDialog( onCancel: (() -> Unit)?, delayMillis: Long = 600, @StringRes manualCancelButton: Int? = null, - op: suspend (android.app.ProgressDialog) -> T -): T = coroutineScope { - val dialog = android.app.ProgressDialog(context, R.style.AppCompatProgressDialogStyle).apply { - setCancelable(onCancel != null) - if (manualCancelButton != null) { - setCancelable(false) - setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(manualCancelButton)) { _, _ -> - Timber.i("Progress dialog cancelled via cancel button") - onCancel?.let { it() } + op: suspend (android.app.ProgressDialog) -> T, +): T = + coroutineScope { + val dialog = + android.app.ProgressDialog(context, R.style.AppCompatProgressDialogStyle).apply { + setCancelable(onCancel != null) + if (manualCancelButton != null) { + setCancelable(false) + setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(manualCancelButton)) { _, _ -> + Timber.i("Progress dialog cancelled via cancel button") + onCancel?.let { it() } + } + } else { + onCancel?.let { + setOnCancelListener { + Timber.i("Progress dialog cancelled via cancel listener") + it() + } + } + } } - } else { - onCancel?.let { - setOnCancelListener { - Timber.i("Progress dialog cancelled via cancel listener") - 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) { + Timber.i( + """Displaying progress dialog: ${delayMillis}ms elapsed; + |cancellable: ${onCancel != null}; + |manualCancel: ${manualCancelButton != null} + | + """.trimMargin(), + ) + dialog.show() + AnkiDroidApp.instance.progressDialogShown = true + dialogIsOurs = true + } else { + Timber.w( + """A progress dialog is already displayed, not displaying progress dialog: + |cancellable: ${onCancel != null}; + |manualCancel: ${manualCancelButton != null} + | + """.trimMargin(), + ) } } + try { + op(dialog) + } finally { + dialogJob.cancel() + dismissDialogIfShowing(dialog) + context.window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + if (dialogIsOurs) { + AnkiDroidApp.instance.progressDialogShown = false + } } } - // 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) { - Timber.i("Displaying progress dialog: ${delayMillis}ms elapsed; cancellable: ${onCancel != null}; manualCancel: ${manualCancelButton != null}") - dialog.show() - AnkiDroidApp.instance.progressDialogShown = true - dialogIsOurs = true - } else { - Timber.w("A progress dialog is already displayed, not displaying progress dialog: ${onCancel != null}; manualCancel: ${manualCancelButton != null}") - } - } - try { - op(dialog) - } finally { - dialogJob.cancel() - dismissDialogIfShowing(dialog) - context.window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) - if (dialogIsOurs) { - AnkiDroidApp.instance.progressDialogShown = false - } - } -} private fun dismissDialogIfShowing(dialog: Dialog) { try { @@ -443,13 +472,14 @@ private fun dismissDialogIfShowing(dialog: Dialog) { private suspend fun monitorProgress( backend: Backend, extractProgress: ProgressContext.() -> Unit, - updateUi: ProgressContext.() -> Unit + updateUi: ProgressContext.() -> Unit, ) { val state = ProgressContext(Progress.getDefaultInstance()) while (true) { - state.progress = withContext(Dispatchers.IO) { - backend.latestProgress() - } + state.progress = + withContext(Dispatchers.IO) { + backend.latestProgress() + } state.extractProgress() // on main thread, so op can update UI withContext(Dispatchers.Main) { @@ -466,7 +496,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 @@ -475,9 +505,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) } @@ -514,14 +545,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 22f807dbfeea..77839706af1c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt @@ -44,7 +44,6 @@ import org.acra.sender.HttpSender import timber.log.Timber object CrashReportService { - // ACRA constants used for stored preferences const val FEEDBACK_REPORT_KEY = "reportErrorMode" const val FEEDBACK_REPORT_ASK = "2" @@ -53,16 +52,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 @@ -78,74 +78,77 @@ 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 { @@ -258,23 +261,40 @@ 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.") @@ -290,11 +310,12 @@ object CrashReportService { } } - fun isProperServiceProcess(): Boolean { - return ACRA.isACRASenderServiceProcess() - } + fun isProperServiceProcess(): Boolean = 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) @@ -316,7 +337,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) @@ -353,7 +377,7 @@ object CrashReportService { deleteACRALimiterData(activity) sendExceptionReport( UserSubmittedException(EXCEPTION_MESSAGE), - "AnkiDroidApp.HelpDialog" + "AnkiDroidApp.HelpDialog", ) true } else { @@ -367,9 +391,10 @@ object CrashReportService { * @param activity the Activity used for Context access when interrogating ACRA reports * @return the timestamp of the most recent report, or -1 if no reports at all */ - private fun getTimestampOfLastReport(activity: AnkiActivity): Long { - return LimiterData.load(activity).reportMetadata + private fun getTimestampOfLastReport(activity: AnkiActivity): Long = + LimiterData + .load(activity) + .reportMetadata .filter { it.exceptionClass == UserSubmittedException::class.java.name } .maxOfOrNull { it.timestamp?.timeInMillis ?: -1L } ?: -1L - } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CustomActionModeCallback.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CustomActionModeCallback.kt index 65c3fd414770..49fba5c93cfd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CustomActionModeCallback.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CustomActionModeCallback.kt @@ -30,15 +30,19 @@ class CustomActionModeCallback( private val isClozeType: Boolean, private val clozeMenuTitle: String, private val clozeMenuId: Int, - private val onActionItemSelected: (mode: ActionMode, item: MenuItem) -> Boolean + private val onActionItemSelected: (mode: ActionMode, item: MenuItem) -> Boolean, ) : ActionMode.Callback { private val setLanguageId = View.generateViewId() - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - return true - } + override fun onCreateActionMode( + mode: ActionMode, + menu: Menu, + ): Boolean = true - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + override fun onPrepareActionMode( + mode: ActionMode, + menu: Menu, + ): Boolean { // Adding the cloze deletion floating context menu item, but only once. if (menu.findItem(clozeMenuId) != null) { return false @@ -59,15 +63,16 @@ class CustomActionModeCallback( Menu.NONE, clozeMenuId, 0, - clozeMenuTitle + clozeMenuTitle, ) } return initialSize != menu.size() } - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return onActionItemSelected(mode, item) - } + override fun onActionItemClicked( + mode: ActionMode, + item: MenuItem, + ): Boolean = onActionItemSelected(mode, item) override fun onDestroyActionMode(mode: ActionMode) { // Left empty on purpose 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/DayRolloverHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DayRolloverHandler.kt index 53b33572ee6a..ae31aa9f62e0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DayRolloverHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DayRolloverHandler.kt @@ -55,8 +55,7 @@ object DayRolloverHandler : BroadcastReceiver() { /** Receive an event each minute AND for time/timezone changes */ fun listenForRolloverEvents(context: Context) { - fun register(filter: IntentFilter) = - ContextCompat.registerReceiver(context, DayRolloverHandler, filter, RECEIVER_EXPORTED) + fun register(filter: IntentFilter) = ContextCompat.registerReceiver(context, DayRolloverHandler, filter, RECEIVER_EXPORTED) Timber.d("listening for rollover events") // ACTION_TIME_TICK occurs every time the displayed time changes (once per minute) @@ -66,7 +65,10 @@ object DayRolloverHandler : BroadcastReceiver() { register(IntentFilter(ACTION_TIMEZONE_CHANGED)) } - override fun onReceive(context: Context?, intent: Intent?) { + override fun onReceive( + context: Context?, + intent: Intent?, + ) { // potential race condition if a timezone/tick change occur simultaneously // the outcome would be two calls to notifySubscribers, which is acceptable Timber.v("received ${intent?.action}") diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index b25fb097cc1e..9c71461a0702 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -260,6 +260,7 @@ open class DeckPicker : private var progressDialog: android.app.ProgressDialog? = null private var studyoptionsFrame: View? = null // not lateInit - can be null + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) lateinit var recyclerView: RecyclerView private lateinit var recyclerViewLayoutManager: LinearLayoutManager @@ -276,15 +277,19 @@ open class DeckPicker : private var recommendOneWaySync = false var activeSnackBar: Snackbar? = null - private val activeSnackbarCallback = object : BaseTransientBottomBar.BaseCallback() { - override fun onShown(transientBottomBar: Snackbar?) { - activeSnackBar = transientBottomBar - } + private val activeSnackbarCallback = + object : BaseTransientBottomBar.BaseCallback() { + override fun onShown(transientBottomBar: Snackbar?) { + activeSnackBar = transientBottomBar + } - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - activeSnackBar = null + override fun onDismissed( + transientBottomBar: Snackbar?, + event: Int, + ) { + activeSnackBar = null + } } - } override val baseSnackbarBuilder: SnackbarBuilder = { anchorView = findViewById(R.id.fab_main) addCallback(activeSnackbarCallback) @@ -330,62 +335,70 @@ 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) { - syncOnResume = true - } - } - ) + private val loginForSyncLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + if (it.resultCode == RESULT_OK) { + syncOnResume = 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) { - lifecycleScope.launch { - withProgress(message = getString(R.string.import_preparing_file)) { - withContext(Dispatchers.IO) { - onSelectedPackageToImport(it.data!!) + private val apkgFileImportResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + DeckPickerActivityResultCallback { + if (it.resultCode == RESULT_OK) { + lifecycleScope.launch { + withProgress(message = getString(R.string.import_preparing_file)) { + withContext(Dispatchers.IO) { + 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() @@ -410,12 +423,17 @@ open class DeckPicker : // ---------------------------------------------------------------------------- // LISTENERS // ---------------------------------------------------------------------------- - private val deckExpanderClickListener = View.OnClickListener { view: View -> - launchCatchingTask { toggleDeckExpand(view.tag as Long) } - } + private val deckExpanderClickListener = + View.OnClickListener { view: View -> + launchCatchingTask { toggleDeckExpand(view.tag as Long) } + } private val deckClickListener = View.OnClickListener { v: View -> onDeckClick(v, DeckSelectionType.DEFAULT) } private val countsClickListener = 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 { @@ -429,41 +447,45 @@ open class DeckPicker : } } - private val deckContextAndLongClickListener = OnContextAndLongClickListener { v -> - val deckId = v.tag as DeckId - showDeckPickerContextMenu(deckId) - true - } + private val deckContextAndLongClickListener = + OnContextAndLongClickListener { v -> + val deckId = v.tag as DeckId + showDeckPickerContextMenu(deckId) + true + } private fun showDeckPickerContextMenu(deckId: DeckId) { launchCatchingTask { - val (deckName, isDynamic, hasBuriedInDeck) = withCol { - decks.select(deckId) - Triple( - decks.name(deckId), - decks.isFiltered(deckId), - sched.haveBuried() - ) - } + val (deckName, isDynamic, hasBuriedInDeck) = + withCol { + decks.select(deckId) + Triple( + decks.name(deckId), + decks.isFiltered(deckId), + sched.haveBuried(), + ) + } updateDeckList() // focus has changed showDialogFragment( DeckPickerContextMenu.newInstance( id = deckId, name = deckName, isDynamic = isDynamic, - hasBuriedInDeck = hasBuriedInDeck - ) + hasBuriedInDeck = hasBuriedInDeck, + ), ) } } - private val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { - Timber.i("notification permission: %b", it) - } + private val notificationPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { + Timber.i("notification permission: %b", it) + } // ---------------------------------------------------------------------------- // ANDROID ACTIVITY METHODS // ---------------------------------------------------------------------------- + /** Called when the activity is first created. */ @Throws(SQLException::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -535,7 +557,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) { @@ -545,26 +568,28 @@ open class DeckPicker : exportingDelegate.onRestoreInstanceState(savedInstanceState) // create and set an adapter for the RecyclerView - deckListAdapter = DeckAdapter(layoutInflater, this).apply { - setDeckClickListener(deckClickListener) - setCountsClickListener(countsClickListener) - setDeckExpanderClickListener(deckExpanderClickListener) - setDeckContextAndLongClickListener(deckContextAndLongClickListener) - enablePartialTransparencyForBackground(hasDeckPickerBackground) - } + deckListAdapter = + DeckAdapter(layoutInflater, this).apply { + setDeckClickListener(deckClickListener) + setCountsClickListener(countsClickListener) + setDeckExpanderClickListener(deckExpanderClickListener) + setDeckContextAndLongClickListener(deckContextAndLongClickListener) + enablePartialTransparencyForBackground(hasDeckPickerBackground) + } recyclerView.adapter = deckListAdapter - pullToSyncWrapper = findViewById(R.id.pull_to_sync_wrapper).apply { - setDistanceToTriggerSync(SWIPE_TO_SYNC_TRIGGER_DISTANCE) - setOnRefreshListener { - Timber.i("Pull to Sync: Syncing") - pullToSyncWrapper.isRefreshing = false - sync() - } - viewTreeObserver.addOnScrollChangedListener { - pullToSyncWrapper.isEnabled = recyclerViewLayoutManager.findFirstCompletelyVisibleItemPosition() == 0 + pullToSyncWrapper = + findViewById(R.id.pull_to_sync_wrapper).apply { + setDistanceToTriggerSync(SWIPE_TO_SYNC_TRIGGER_DISTANCE) + setOnRefreshListener { + Timber.i("Pull to Sync: Syncing") + pullToSyncWrapper.isRefreshing = false + sync() + } + viewTreeObserver.addOnScrollChangedListener { + pullToSyncWrapper.isEnabled = recyclerViewLayoutManager.findFirstCompletelyVisibleItemPosition() == 0 + } } - } // Setup the FloatingActionButtons, should work everywhere with min API >= 15 floatingActionMenu = DeckPickerFloatingActionMenu(this, view, this) @@ -576,11 +601,12 @@ open class DeckPicker : supportFragmentManager.setFragmentResultListener(DeckPickerContextMenu.REQUEST_KEY_CONTEXT_MENU, this) { requestKey, arguments -> when (requestKey) { - DeckPickerContextMenu.REQUEST_KEY_CONTEXT_MENU -> handleContextMenuSelection( - arguments.getSerializableCompat(DeckPickerContextMenu.CONTEXT_MENU_DECK_OPTION) - ?: error("Unable to retrieve selected context menu option"), - arguments.getLong(DeckPickerContextMenu.CONTEXT_MENU_DECK_ID, -1) - ) + DeckPickerContextMenu.REQUEST_KEY_CONTEXT_MENU -> + handleContextMenuSelection( + arguments.getSerializableCompat(DeckPickerContextMenu.CONTEXT_MENU_DECK_OPTION) + ?: error("Unable to retrieve selected context menu option"), + arguments.getLong(DeckPickerContextMenu.CONTEXT_MENU_DECK_ID, -1), + ) else -> error("Unexpected fragment result key! Did you forget to update DeckPicker?") } } @@ -588,40 +614,42 @@ open class DeckPicker : pullToSyncWrapper.configureView( this, IMPORT_MIME_TYPES, - DropHelper.Options.Builder() + DropHelper.Options + .Builder() .setHighlightColor(R.color.material_lime_green_A700) .setHighlightCornerRadiusPx(0) .build(), - onReceiveContentListener + onReceiveContentListener, ) } - private val onReceiveContentListener = OnReceiveContentListener { _, payload -> - val (uriContent, remaining) = payload.partition { item -> item.uri != null } + private val onReceiveContentListener = + OnReceiveContentListener { _, payload -> + val (uriContent, remaining) = payload.partition { item -> item.uri != null } - val clip = uriContent?.clip ?: return@OnReceiveContentListener remaining - val uri = clip.getItemAt(0).uri - if (!ImportUtils.FileImporter().isValidImportType(this, uri)) { - ImportResult.fromErrorString(getString(R.string.import_log_no_apkg)) - return@OnReceiveContentListener remaining - } + val clip = uriContent?.clip ?: return@OnReceiveContentListener remaining + val uri = clip.getItemAt(0).uri + if (!ImportUtils.FileImporter().isValidImportType(this, uri)) { + ImportResult.fromErrorString(getString(R.string.import_log_no_apkg)) + return@OnReceiveContentListener remaining + } + + try { + // Intent is nullable because `clip.getItemAt(0).intent` always returns null + ImportUtils.FileImporter().handleContentProviderFile(this, uri) + onResume() + } catch (e: Exception) { + Timber.w(e) + CrashReportService.sendExceptionReport(e, "DeckPicker::onReceiveContent") + return@OnReceiveContentListener remaining + } - try { - // Intent is nullable because `clip.getItemAt(0).intent` always returns null - ImportUtils.FileImporter().handleContentProviderFile(this, uri) - onResume() - } catch (e: Exception) { - Timber.w(e) - CrashReportService.sendExceptionReport(e, "DeckPicker::onReceiveContent") return@OnReceiveContentListener remaining } - return@OnReceiveContentListener remaining - } - private fun handleContextMenuSelection( selectedOption: DeckPickerContextMenuOption, - deckId: DeckId + deckId: DeckId, ) { when (selectedOption) { DeckPickerContextMenuOption.DELETE_DECK -> { @@ -715,16 +743,17 @@ open class DeckPicker : Timber.d("handleStartup: Continuing after permission granted") val failure = InitialActivity.getStartupFailureType(this) - startupError = 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 - } + startupError = + 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 @@ -750,19 +779,21 @@ open class DeckPicker : Timber.i("Displaying database locked error") showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_DB_LOCKED) } - is WebviewFailed -> AlertDialog.Builder(this).show { - title(R.string.ankidroid_init_failed_webview_title) - message( - text = getString( - R.string.ankidroid_init_failed_webview, - AnkiDroidApp.webViewErrorMessage + is WebviewFailed -> + AlertDialog.Builder(this).show { + title(R.string.ankidroid_init_failed_webview_title) + message( + text = + getString( + R.string.ankidroid_init_failed_webview, + AnkiDroidApp.webViewErrorMessage, + ), ) - ) - positiveButton(R.string.close) { - closeCollectionAndFinish() + positiveButton(R.string.close) { + closeCollectionAndFinish() + } + cancelable(false) } - cancelable(false) - } is DiskFull -> displayNoStorageError() is DBError -> displayDatabaseFailure(CustomExceptionData.fromException(failure.exception)) else -> displayDatabaseFailure() @@ -770,14 +801,16 @@ open class DeckPicker : } private fun showDirectoryNotAccessibleDialog() { - val contentView = TextView(this).apply { - autoLinkMask = Linkify.WEB_URLS - linksClickable = true - text = getString( - R.string.directory_inaccessible_info, - getString(R.string.link_full_storage_access) - ) - } + val contentView = + TextView(this).apply { + autoLinkMask = Linkify.WEB_URLS + linksClickable = true + text = + getString( + R.string.directory_inaccessible_info, + getString(R.string.link_full_storage_access), + ) + } AlertDialog.Builder(this).show { title(R.string.directory_inaccessible) customView( @@ -785,7 +818,7 @@ open class DeckPicker : convertDpToPixel(16F, this@DeckPicker).toInt(), 0, convertDpToPixel(32F, this@DeckPicker).toInt(), - convertDpToPixel(32F, this@DeckPicker).toInt() + convertDpToPixel(32F, this@DeckPicker).toInt(), ) positiveButton(R.string.open_settings) { val settingsIntent = PreferencesActivity.getIntent(this@DeckPicker, AdvancedSettingsFragment::class) @@ -863,19 +896,20 @@ 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() - updateSearchVisibilityFromState(menu) - if (!fragmented) { - updateMenuFromState(menu) + createMenuJob = + launchCatchingTask { + updateMenuState() + updateSearchVisibilityFromState(menu) + if (!fragmented) { + updateMenuFromState(menu) + } } - } return super.onCreateOptionsMenu(menu) } override fun onPrepareOptionsMenu(menu: Menu): Boolean { menu.findItem(R.id.action_custom_study)?.setShowAsAction( - if (fragmented) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + if (fragmented) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER, ) return super.onPrepareOptionsMenu(menu) } @@ -885,72 +919,78 @@ open class DeckPicker : syncMediaProgressJob?.cancel() val syncItem = menu.findItem(R.id.action_sync) - val progressIndicator = syncItem.actionView - ?.findViewById(R.id.progress_indicator) + val progressIndicator = + syncItem.actionView + ?.findViewById(R.id.progress_indicator) val workManager = WorkManager.getInstance(this) val flow = workManager.getWorkInfosForUniqueWorkFlow(UniqueWorkNames.SYNC_MEDIA) - syncMediaProgressJob = lifecycleScope.launch { - flow.flowWithLifecycle(lifecycle).collectLatest { - val workInfo = it.lastOrNull() - if (workInfo?.state == WorkInfo.State.RUNNING && progressIndicator?.isVisible == false) { - Timber.i("DeckPicker: Showing media sync progress indicator") - progressIndicator.isVisible = true - } else if (progressIndicator?.isVisible == true) { - Timber.i("DeckPicker: Hiding media sync progress indicator") - progressIndicator.isVisible = false + syncMediaProgressJob = + lifecycleScope.launch { + flow.flowWithLifecycle(lifecycle).collectLatest { + val workInfo = it.lastOrNull() + if (workInfo?.state == WorkInfo.State.RUNNING && progressIndicator?.isVisible == false) { + Timber.i("DeckPicker: Showing media sync progress indicator") + progressIndicator.isVisible = true + } else if (progressIndicator?.isVisible == true) { + Timber.i("DeckPicker: Hiding media sync progress indicator") + progressIndicator.isVisible = false + } } } - } } 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 - floatingActionMenu.hideFloatingActionButton() - return true - } + 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 + floatingActionMenu.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 - floatingActionMenu.showFloatingActionButton() - 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 + floatingActionMenu.showFloatingActionButton() + return true + } + }, + ) (menuItem.actionView as AccessibleSearchView).run { queryHint = getString(R.string.search_decks) - setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - clearFocus() - return true - } + 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 - if (adapter == null || adapter.filter == null) { - Timber.w( - "DeckPicker.onQueryTextChange: adapter is null: %s, filter is null: %s, adapter type: %s", - adapter == null, - adapter?.filter == null, - adapter?.javaClass?.simpleName ?: "Unknown" - ) - CrashReportService.sendExceptionReport( - Exception("DeckPicker.onQueryTextChanged with unexpected null adapter or filter. Carefully examine logcat"), - "DeckPicker" - ) + override fun onQueryTextChange(newText: String): Boolean { + val adapter = recyclerView.adapter as? Filterable + if (adapter == null || adapter.filter == null) { + Timber.w( + "DeckPicker.onQueryTextChange: adapter is null: %s, filter is null: %s, adapter type: %s", + adapter == null, + adapter?.filter == null, + adapter?.javaClass?.simpleName ?: "Unknown", + ) + CrashReportService.sendExceptionReport( + Exception("DeckPicker.onQueryTextChanged with unexpected null adapter or filter. Carefully examine logcat"), + "DeckPicker", + ) + return true + } + adapter.filter.filter(newText) return true } - adapter.filter.filter(newText) - return true - } - }) + }, + ) } searchDecksIcon = menuItem } @@ -964,10 +1004,11 @@ open class DeckPicker : private suspend fun updateUndoMenuState() { withOpenColOrNull { - optionsMenuState = optionsMenuState?.copy( - undoLabel = undoLabel(), - undoAvailable = undoAvailable() - ) + optionsMenuState = + optionsMenuState?.copy( + undoLabel = undoLabel(), + undoAvailable = undoAvailable(), + ) } } @@ -980,7 +1021,7 @@ open class DeckPicker : private fun updateUndoLabelFromState( menuItem: MenuItem, undoLabel: String?, - undoAvailable: Boolean + undoAvailable: Boolean, ) { menuItem.run { if (undoLabel != null && undoAvailable) { @@ -992,14 +1033,19 @@ open class DeckPicker : } } - private fun updateSyncIconFromState(menuItem: MenuItem, state: OptionsMenuState) { - val provider = MenuItemCompat.getActionProvider(menuItem) as? SyncActionProvider - ?: return - val tooltipText = when (state.syncIcon) { - SyncIconState.Normal, SyncIconState.PendingChanges -> R.string.button_sync - SyncIconState.OneWay -> R.string.sync_menu_title_one_way_sync - SyncIconState.NotLoggedIn -> R.string.sync_menu_title_no_account - } + private fun updateSyncIconFromState( + menuItem: MenuItem, + state: OptionsMenuState, + ) { + val provider = + MenuItemCompat.getActionProvider(menuItem) as? SyncActionProvider + ?: return + val tooltipText = + when (state.syncIcon) { + SyncIconState.Normal, SyncIconState.PendingChanges -> R.string.button_sync + SyncIconState.OneWay -> R.string.sync_menu_title_one_way_sync + SyncIconState.NotLoggedIn -> R.string.sync_menu_title_no_account + } provider.setTooltipText(getString(tooltipText)) when (state.syncIcon) { SyncIconState.Normal -> { @@ -1021,15 +1067,16 @@ open class DeckPicker : @VisibleForTesting suspend fun updateMenuState() { - optionsMenuState = withOpenColOrNull { - val searchIcon = decks.count() >= 10 - val undoLabel = undoLabel() - val undoAvailable = undoAvailable() - Triple(searchIcon, undoLabel, undoAvailable) - }?.let { (searchIcon, undoLabel, undoAvailable) -> - val syncIcon = fetchSyncStatus() - OptionsMenuState(searchIcon, undoLabel, syncIcon, undoAvailable) - } + optionsMenuState = + withOpenColOrNull { + val searchIcon = decks.count() >= 10 + val undoLabel = undoLabel() + val undoAvailable = undoAvailable() + Triple(searchIcon, undoLabel, undoAvailable) + }?.let { (searchIcon, undoLabel, undoAvailable) -> + val syncIcon = fetchSyncStatus() + OptionsMenuState(searchIcon, undoLabel, syncIcon, undoAvailable) + } } private suspend fun fetchSyncStatus(): SyncIconState { @@ -1122,7 +1169,8 @@ open class DeckPicker : } fun showCreateFilteredDeckDialog() { - 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.onNewDeckCreated = { // a filtered deck was created openFilteredDeckOptions() @@ -1236,16 +1284,19 @@ open class DeckPicker : */ suspend fun areThereChangesToSync(): Boolean { val auth = syncAuth() ?: return false - val status = withContext(Dispatchers.IO) { - CollectionManager.getBackend().syncStatus(auth) - }.required + val status = + withContext(Dispatchers.IO) { + CollectionManager.getBackend().syncStatus(auth) + }.required return when (status) { SyncStatusResponse.Required.NO_CHANGES, SyncStatusResponse.Required.UNRECOGNIZED, - null -> false + null, + -> false SyncStatusResponse.Required.FULL_SYNC, - SyncStatusResponse.Required.NORMAL_SYNC -> true + SyncStatusResponse.Required.NORMAL_SYNC, + -> true } } @@ -1257,8 +1308,9 @@ open class DeckPicker : val isAutoSyncEnabled = sharedPrefs().getBoolean("automaticSyncMode", false) - val isBlockedByMeteredConnection = !sharedPrefs().getBoolean(getString(R.string.metered_sync_key), false) && - isActiveNetworkMetered() + val isBlockedByMeteredConnection = + !sharedPrefs().getBoolean(getString(R.string.metered_sync_key), false) && + isActiveNetworkMetered() when { !isAutoSyncEnabled -> Timber.d("autoSync: not enabled") @@ -1300,16 +1352,18 @@ open class DeckPicker : } else { if (!preferences.getBoolean( "exitViaDoubleTapBack", - false - ) || backButtonPressedToExit + false, + ) || + backButtonPressedToExit ) { // can't use launchCatchingTask because any errors // would need to be shown in the UI - lifecycleScope.launch { - automaticSync(runInBackground = true) - }.invokeOnCompletion { - finish() - } + lifecycleScope + .launch { + automaticSync(runInBackground = true) + }.invokeOnCompletion { + finish() + } } else { showSnackbar(R.string.back_pressed_once, Snackbar.LENGTH_SHORT) } @@ -1321,7 +1375,10 @@ open class DeckPicker : } } - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { if (toolbarSearchView?.hasFocus() == true) { Timber.d("Skipping keypress: search action bar is focused") return true @@ -1453,22 +1510,25 @@ open class DeckPicker : /** * Displays a confirmation dialog for deleting deck. */ - private fun showDeleteDeckConfirmationDialog() = launchCatchingTask { - val (deckName, totalCards, isFilteredDeck) = withCol { - Triple( - decks.name(focusedDeck), - decks.cardCount(focusedDeck, includeSubdecks = true), - decks.isFiltered(focusedDeck) - ) + private fun showDeleteDeckConfirmationDialog() = + launchCatchingTask { + val (deckName, totalCards, isFilteredDeck) = + withCol { + Triple( + decks.name(focusedDeck), + decks.cardCount(focusedDeck, includeSubdecks = true), + decks.isFiltered(focusedDeck), + ) + } + val confirmDeleteDeckDialog = + DeckPickerConfirmDeleteDeckDialog.newInstance( + deckName = deckName, + deckId = focusedDeck, + totalCards = totalCards, + isFilteredDeck = isFilteredDeck, + ) + showDialogFragment(confirmDeleteDeckDialog) } - val confirmDeleteDeckDialog = DeckPickerConfirmDeleteDeckDialog.newInstance( - deckName = deckName, - deckId = focusedDeck, - totalCards = totalCards, - isFilteredDeck = isFilteredDeck - ) - showDialogFragment(confirmDeleteDeckDialog) - } /** * Perform the following tasks: @@ -1492,11 +1552,12 @@ open class DeckPicker : // If libanki determines it's necessary to confirm the one-way 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(OneWaySyncDialog(message).toMessage()) } @@ -1527,7 +1588,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 @@ -1561,13 +1625,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 @@ -1671,7 +1736,7 @@ open class DeckPicker : // #16061. We have to queue snackbar to avoid the misaligned snackbar showed from onCreate() private fun postSnackbar( text: CharSequence, - duration: Int = Snackbar.LENGTH_LONG + duration: Int = Snackbar.LENGTH_LONG, ) { val view: View? = findViewById(R.id.root_layout) if (view != null) { @@ -1688,27 +1753,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() @@ -1727,7 +1796,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)) } @@ -1744,7 +1816,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) } @@ -1760,13 +1835,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@withCol) + val result = + withProgress(resources.getString(R.string.backup_repair_deck_progress)) { + withCol { + Timber.i("RepairCollection: Closing collection") + close() + BackupManager.repairCollection(this@withCol) + } } - } if (!result) { showThemedToast(this@DeckPicker, resources.getString(R.string.deck_repair_error), true) showCollectionErrorDialog() @@ -1816,12 +1892,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@withCol, unused) } - } + val noOfDeletedFiles = + withProgress(resources.getString(R.string.delete_media_message)) { + withCol { deleteMedia(this@withCol, 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), ) } } @@ -1888,7 +1965,7 @@ open class DeckPicker : preferences.edit { putBoolean( getString(R.string.metered_sync_key), - isCheckboxChecked + isCheckboxChecked, ) } } @@ -1942,10 +2019,12 @@ open class DeckPicker : /** * Refresh the deck picker when the SD card is inserted. */ - override val broadcastsActions = super.broadcastsActions + mapOf( - SdCardReceiver.MEDIA_MOUNT - to { ActivityCompat.recreate(this) } - ) + override val broadcastsActions = + super.broadcastsActions + + mapOf( + SdCardReceiver.MEDIA_MOUNT + to { ActivityCompat.recreate(this) }, + ) fun openAnkiWebSharedDecks() { val intent = Intent(this, SharedDecksActivity::class.java) @@ -1958,7 +2037,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) @@ -2014,10 +2095,14 @@ 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) { - setAction(R.string.menu_add) { addNote() } - } + private suspend fun handleDeckSelection( + did: DeckId, + selectionType: DeckSelectionType, + ) { + fun showEmptyDeckSnackbar() = + showSnackbar(R.string.empty_deck) { + setAction(R.string.menu_add) { addNote() } + } /** Check if we need to update the fragment or update the deck list */ fun updateUi() { @@ -2051,7 +2136,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, + -> { onDeckCompleted() } CompletedDeckStatus.EMPTY_REGULAR_DECK -> { @@ -2092,20 +2178,25 @@ open class DeckPicker : } Timber.d("updateDeckList") loadDeckCounts?.cancel() - loadDeckCounts = launchCatchingTask { - withProgress { - Timber.d("Refreshing deck list") - val (deckDueTree, collectionHasNoCards) = withCol { - Pair(sched.deckDueTree(), isEmpty) - } - onDecksLoaded(deckDueTree, collectionHasNoCards) + loadDeckCounts = + launchCatchingTask { + withProgress { + Timber.d("Refreshing deck list") + val (deckDueTree, collectionHasNoCards) = + withCol { + Pair(sched.deckDueTree(), isEmpty) + } + onDecksLoaded(deckDueTree, collectionHasNoCards) - updateUndoMenuState() + updateUndoMenuState() + } } - } } - private fun onDecksLoaded(result: DeckNode, collectionHasNoCards: Boolean) { + private fun onDecksLoaded( + result: DeckNode, + collectionHasNoCards: Boolean, + ) { Timber.i("Updating deck list UI") hideProgressBar() // Make sure the fragment is visible @@ -2118,9 +2209,10 @@ open class DeckPicker : // Update the mini statistics bar as well reviewSummaryTextView.setSingleLine() launchCatchingTask { - reviewSummaryTextView.text = withCol { - sched.studiedToday() - } + reviewSummaryTextView.text = + withCol { + sched.studiedToday() + } } Timber.d("Startup - Deck List UI Completed") } @@ -2141,11 +2233,12 @@ open class DeckPicker : deckPickerContent.visibility = if (isEmpty) View.GONE else View.VISIBLE noDecksPlaceholder.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 = deckPickerContent.visibility == View.VISIBLE val placeholderShown = noDecksPlaceholder.visibility == View.VISIBLE if (isEmpty) { @@ -2153,11 +2246,21 @@ open class DeckPicker : fadeOut(deckPickerContent, shortAnimDuration, translation) } if (!placeholderShown) { - fadeIn(noDecksPlaceholder, shortAnimDuration, translation).startDelay = if (decksListShown) shortAnimDuration * 2.toLong() else 0.toLong() + fadeIn(noDecksPlaceholder, shortAnimDuration, translation).startDelay = + if (decksListShown) { + shortAnimDuration * 2.toLong() + } else { + 0.toLong() + } } } else { if (!decksListShown) { - fadeIn(deckPickerContent, shortAnimDuration, translation).startDelay = if (placeholderShown) shortAnimDuration * 2.toLong() else 0.toLong() + fadeIn(deckPickerContent, shortAnimDuration, translation).startDelay = + if (placeholderShown) { + shortAnimDuration * 2.toLong() + } else { + 0.toLong() + } } if (placeholderShown) { fadeOut(noDecksPlaceholder, shortAnimDuration, translation) @@ -2185,11 +2288,12 @@ open class DeckPicker : val res = resources if (due != null && supportActionBar != null) { - val subTitle = if (due == 0) { - null - } else { - res.getQuantityString(R.plurals.widget_cards_due, due, due) - } + val subTitle = + if (due == 0) { + null + } else { + res.getQuantityString(R.plurals.widget_cards_due, due, due) + } supportActionBar!!.subtitle = subTitle val toolbar = findViewById(R.id.toolbar) @@ -2215,7 +2319,9 @@ open class DeckPicker : startActivity(i) } else { // otherwise open regular options - val intent = com.ichi2.anki.pages.DeckOptions.getIntent(this, did) + val intent = + com.ichi2.anki.pages.DeckOptions + .getIntent(this, did) startActivity(intent) } } @@ -2224,16 +2330,20 @@ open class DeckPicker : ExportDialogFragment.newInstance(did).show(supportFragmentManager, "exportOptions") } - private fun createIcon(context: Context, did: DeckId) { + private fun createIcon( + context: Context, + did: DeckId, + ) { // This code should not be reachable with lower versions - val shortcut = ShortcutInfoCompat.Builder(this, did.toString()) - .setIntent( - intentToReviewDeckFromShorcuts(context, 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( + intentToReviewDeckFromShorcuts(context, 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) @@ -2298,14 +2408,15 @@ open class DeckPicker : * Use [.confirmDeckDeletion] for a confirmation dialog * @param did the deck to delete */ - fun deleteDeck(did: DeckId): Job { - return launchCatchingTask { + fun deleteDeck(did: DeckId): Job = + launchCatchingTask { val deckName = withCol { decks.get(did)!!.name } - val changes = withProgress(resources.getString(R.string.delete_deck)) { - undoableOp { - decks.remove(listOf(did)) + val changes = + withProgress(resources.getString(R.string.delete_deck)) { + undoableOp { + decks.remove(listOf(did)) + } } - } // After deletion: decks.current() reverts to Default, necessitating `focusedDeck` // to match and avoid unnecessary scrolls in `renderPage()`. focusedDeck = Consts.DEFAULT_DECK_ID @@ -2313,7 +2424,6 @@ open class DeckPicker : setAction(R.string.undo) { undo() } } } - } @NeedsTest("14285: regression test to ensure UI is updated after this call") fun rebuildFiltered(did: DeckId) { @@ -2356,11 +2466,12 @@ open class DeckPicker : } private fun openReviewer() { - val intent = if (sharedPrefs().getBoolean("newReviewer", false)) { - ReviewerFragment.getIntent(this) - } else { - Intent(this, Reviewer::class.java) - } + val intent = + if (sharedPrefs().getBoolean("newReviewer", false)) { + ReviewerFragment.getIntent(this) + } else { + Intent(this, Reviewer::class.java) + } reviewLauncher.launch(intent) } @@ -2378,11 +2489,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()) { @@ -2430,9 +2542,7 @@ open class DeckPicker : /** * Check if at least one deck is being displayed. */ - fun hasAtLeastOneDeckBeingDisplayed(): Boolean { - return deckListAdapter.itemCount > 0 && recyclerViewLayoutManager.getChildAt(0) != null - } + fun hasAtLeastOneDeckBeingDisplayed(): Boolean = deckListAdapter.itemCount > 0 && recyclerViewLayoutManager.getChildAt(0) != null private enum class DeckSelectionType { /** Show study options if fragmented, otherwise, review */ @@ -2442,32 +2552,33 @@ open class DeckPicker : SHOW_STUDY_OPTIONS, /** Always open reviewer (keyboard shortcut) */ - SKIP_STUDY_OPTIONS + SKIP_STUDY_OPTIONS, } override val shortcuts - get() = ShortcutGroup( - listOf( - shortcut("A", R.string.menu_add_note), - shortcut("B", R.string.card_browser_context_menu), - shortcut("Y", R.string.pref_cat_sync), - shortcut("/", R.string.deck_conf_cram_search), - shortcut("S", Translations::decksStudyDeck), - shortcut("T", R.string.open_statistics), - shortcut("C", R.string.check_db), - shortcut("D", R.string.new_deck), - shortcut("F", R.string.new_dynamic_deck), - shortcut("DEL", R.string.delete_deck_title), - shortcut("Shift+DEL", R.string.delete_deck_without_confirmation), - shortcut("R", R.string.rename_deck), - shortcut("P", R.string.open_settings), - shortcut("M", R.string.check_media), - shortcut("Ctrl+E", R.string.export_collection), - shortcut("Ctrl+Shift+I", R.string.menu_import), - shortcut("Ctrl+Shift+N", R.string.model_browser_label) - ), - R.string.deck_picker_group - ) + get() = + ShortcutGroup( + listOf( + shortcut("A", R.string.menu_add_note), + shortcut("B", R.string.card_browser_context_menu), + shortcut("Y", R.string.pref_cat_sync), + shortcut("/", R.string.deck_conf_cram_search), + shortcut("S", Translations::decksStudyDeck), + shortcut("T", R.string.open_statistics), + shortcut("C", R.string.check_db), + shortcut("D", R.string.new_deck), + shortcut("F", R.string.new_dynamic_deck), + shortcut("DEL", R.string.delete_deck_title), + shortcut("Shift+DEL", R.string.delete_deck_without_confirmation), + shortcut("R", R.string.rename_deck), + shortcut("P", R.string.open_settings), + shortcut("M", R.string.check_media), + shortcut("Ctrl+E", R.string.export_collection), + shortcut("Ctrl+Shift+I", R.string.menu_import), + shortcut("Ctrl+Shift+N", R.string.model_browser_label), + ), + R.string.deck_picker_group, + ) companion object { /** @@ -2497,20 +2608,35 @@ 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() + return view + .animate() .alpha(1f) .translationY(0f) .setDuration(duration.toLong()) .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() + return view + .animate() .alpha(0f) .translationY(translation) .setDuration(duration.toLong()) @@ -2518,7 +2644,10 @@ open class DeckPicker : } } - override fun opExecuted(changes: OpChanges, handler: Any?) { + override fun opExecuted( + changes: OpChanges, + handler: Any?, + ) { if (changes.studyQueues && handler !== this) { invalidateOptionsMenu() if (!activityPaused) { @@ -2548,9 +2677,7 @@ open class DeckPicker : * * @param did The id of a deck with no pending cards to review */ - private suspend fun queryCompletedDeckCustomStudyAction( - did: DeckId - ): CompletedDeckStatus = + private suspend fun queryCompletedDeckCustomStudyAction(did: DeckId): CompletedDeckStatus = when { withCol { sched.hasCardsTodayAfterStudyAheadLimit() } -> CompletedDeckStatus.LEARN_AHEAD_LIMIT_REACHED withCol { sched.newDue() || sched.revDue() } -> CompletedDeckStatus.LEARN_AHEAD_LIMIT_REACHED @@ -2574,16 +2701,12 @@ 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 { - return apkgFileImportResultLauncher - } + override fun getApkgFileImportResultLauncher(): ActivityResultLauncher = apkgFileImportResultLauncher - override fun getCsvFileImportResultLauncher(): ActivityResultLauncher { - return csvImportResultLauncher - } + override fun getCsvFileImportResultLauncher(): ActivityResultLauncher = csvImportResultLauncher } /** Android's onCreateOptionsMenu does not play well with coroutines, as @@ -2597,20 +2720,21 @@ data class OptionsMenuState( /** If undo is available, a string describing the action. */ val undoLabel: String?, val syncIcon: SyncIconState, - val undoAvailable: Boolean + val undoAvailable: Boolean, ) enum class SyncIconState { Normal, PendingChanges, OneWay, - NotLoggedIn + NotLoggedIn, } -class CollectionLoadingErrorDialog : DialogHandlerMessage( - WhichDialogHandler.MSG_SHOW_COLLECTION_LOADING_ERROR_DIALOG, - "CollectionLoadErrorDialog" -) { +class CollectionLoadingErrorDialog : + DialogHandlerMessage( + WhichDialogHandler.MSG_SHOW_COLLECTION_LOADING_ERROR_DIALOG, + "CollectionLoadErrorDialog", + ) { override fun handleAsyncMessage(activity: AnkiActivity) { // Collection could not be opened activity.showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_LOAD_FAILED) @@ -2619,37 +2743,42 @@ class CollectionLoadingErrorDialog : DialogHandlerMessage( override fun toMessage() = emptyMessage(this.what) } -class OneWaySyncDialog(val message: String?) : DialogHandlerMessage( - which = WhichDialogHandler.MSG_SHOW_ONE_WAY_SYNC_DIALOG, - analyticName = "OneWaySyncDialog" -) { +class OneWaySyncDialog( + val message: String?, +) : DialogHandlerMessage( + which = WhichDialogHandler.MSG_SHOW_ONE_WAY_SYNC_DIALOG, + analyticName = "OneWaySyncDialog", + ) { override fun handleAsyncMessage(activity: AnkiActivity) { // Confirmation dialog for one-way sync val dialog = ConfirmationDialog() - val confirm = Runnable { - // Bypass the check once the user confirms - CollectionManager.getColUnsafe().modSchemaNoCheck() - } + val confirm = + Runnable { + // Bypass the check once the user confirms + CollectionManager.getColUnsafe().modSchemaNoCheck() + } dialog.setConfirm(confirm) dialog.setArgs(message) activity.showDialogFragment(dialog) } - override fun toMessage(): Message = Message.obtain().apply { - what = this@OneWaySyncDialog.what - data = bundleOf("message" to message) - } + override fun toMessage(): Message = + Message.obtain().apply { + what = this@OneWaySyncDialog.what + data = bundleOf("message" to message) + } companion object { - fun fromMessage(message: Message): DialogHandlerMessage = - OneWaySyncDialog(message.data.getString("message")) + fun fromMessage(message: Message): DialogHandlerMessage = OneWaySyncDialog(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 e039fe8a2033..30012b12a32c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt @@ -31,7 +31,7 @@ import timber.log.Timber class DeckPickerFloatingActionMenu( private val context: Context, view: View, - private val deckPicker: DeckPicker + private val deckPicker: DeckPicker, ) { private val fabMain: FloatingActionButton = view.findViewById(R.id.fab_main) private val addSharedLayout: LinearLayout = view.findViewById(R.id.add_shared_layout) @@ -89,7 +89,12 @@ class DeckPickerFloatingActionMenu( // At the end the Image is changed to Add Note Icon fabMain.setImageResource(addNoteIcon) // Shrink back FAB - fabMain.animate().setDuration(70).scaleX(1f).scaleY(1f).start() + fabMain + .animate() + .setDuration(70) + .scaleX(1f) + .scaleY(1f) + .start() }.start() } @@ -155,7 +160,11 @@ class DeckPickerFloatingActionMenu( // At the end the image is changed to Add White Icon fabMain.setImageResource(addWhiteIcon) // Shrink back FAB - fabMain.animate().setDuration(60).scaleX(1f).scaleY(1f) + fabMain + .animate() + .setDuration(60) + .scaleX(1f) + .scaleY(1f) .start() }.start() } @@ -166,36 +175,50 @@ class DeckPickerFloatingActionMenu( addFilteredDeckLayout.animate().alpha(0f).duration = 100 addSharedLayout.animate().translationY(400f).duration = 100 addNoteLabel.animate().translationX(180f).duration = 70 - addDeckLayout.animate().translationY(300f).setDuration(50) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - addSharedLayout.visibility = View.GONE - addDeckLayout.visibility = View.GONE - addFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + addDeckLayout + .animate() + .translationY(300f) + .setDuration(50) + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + addSharedLayout.visibility = View.GONE + addDeckLayout.visibility = View.GONE + addFilteredDeckLayout.visibility = View.GONE + addNoteLabel.visibility = View.GONE + } } - } - - override fun onAnimationCancel(animator: Animator) {} - override fun onAnimationRepeat(animator: Animator) {} - }) - addFilteredDeckLayout.animate().translationY(400f).setDuration(100) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - addSharedLayout.visibility = View.GONE - addDeckLayout.visibility = View.GONE - addFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + + override fun onAnimationCancel(animator: Animator) {} + + override fun onAnimationRepeat(animator: Animator) {} + }, + ) + addFilteredDeckLayout + .animate() + .translationY(400f) + .setDuration(100) + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + addSharedLayout.visibility = View.GONE + addDeckLayout.visibility = View.GONE + addFilteredDeckLayout.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 addSharedLayout.visibility = View.GONE @@ -229,36 +252,50 @@ class DeckPickerFloatingActionMenu( addNoteLabel.animate().alpha(0f).duration = 50 addNoteLabel.animate().translationX(180f).duration = 70 addSharedLayout.animate().translationY(600f).duration = 100 - addDeckLayout.animate().translationY(400f).setDuration(50) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - addSharedLayout.visibility = View.GONE - addDeckLayout.visibility = View.GONE - addFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + addDeckLayout + .animate() + .translationY(400f) + .setDuration(50) + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + addSharedLayout.visibility = View.GONE + addDeckLayout.visibility = View.GONE + addFilteredDeckLayout.visibility = View.GONE + addNoteLabel.visibility = View.GONE + } } - } - - override fun onAnimationCancel(animator: Animator) {} - override fun onAnimationRepeat(animator: Animator) {} - }) - addFilteredDeckLayout.animate().translationY(600f).setDuration(100) - .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) {} - override fun onAnimationEnd(animator: Animator) { - if (!isFABOpen) { - addSharedLayout.visibility = View.GONE - addDeckLayout.visibility = View.GONE - addFilteredDeckLayout.visibility = View.GONE - addNoteLabel.visibility = View.GONE + + override fun onAnimationCancel(animator: Animator) {} + + override fun onAnimationRepeat(animator: Animator) {} + }, + ) + addFilteredDeckLayout + .animate() + .translationY(600f) + .setDuration(100) + .setListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + + override fun onAnimationEnd(animator: Animator) { + if (!isFABOpen) { + addSharedLayout.visibility = View.GONE + addDeckLayout.visibility = View.GONE + addFilteredDeckLayout.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 addSharedLayout.visibility = View.GONE @@ -293,21 +330,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,54 +359,60 @@ 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) - fabMain.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 { + fabMain.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() + } + } + }, + ) fabBGLayout.setOnClickListener { closeFloatingActionMenu(applyRiseAndShrinkAnimation = true) } - val addDeckListener = View.OnClickListener { - if (isFABOpen) { - closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) - deckPicker.showCreateDeckDialog() + val addDeckListener = + View.OnClickListener { + if (isFABOpen) { + closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) + deckPicker.showCreateDeckDialog() + } } - } addDeckButton.setOnClickListener(addDeckListener) addDeckLabel.setOnClickListener(addDeckListener) - val addFilteredDeckListener = View.OnClickListener { - if (isFABOpen) { - closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) - deckPicker.showCreateFilteredDeckDialog() + val addFilteredDeckListener = + View.OnClickListener { + if (isFABOpen) { + closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) + deckPicker.showCreateFilteredDeckDialog() + } } - } 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 896d68a0ad70..ca309530d49e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckSpinnerSelection.kt @@ -57,7 +57,7 @@ import timber.log.Timber */ @KotlinCleanup( "this class is a mess: showAllDecks, AND the adapter seems overly complicated as " + - "only the selected item is visible" + "only the selected item is visible", ) class DeckSpinnerSelection( private val context: AppCompatActivity, @@ -65,9 +65,8 @@ class DeckSpinnerSelection( private val showAllDecks: Boolean, private val alwaysShowDefault: Boolean, private val showFilteredDecks: Boolean, - private val fragmentManagerSupplier: FragmentManagerSupplier = context.asFragmentManagerSupplier() + private val fragmentManagerSupplier: FragmentManagerSupplier = context.asFragmentManagerSupplier(), ) { - private var deckDropDownAdapter: DeckDropDownAdapter? = null // This should be deckDropDownAdapter.decks @@ -75,7 +74,10 @@ class DeckSpinnerSelection( private var dropDownDecks: MutableList? = null @MainThread // spinner.adapter - fun initializeActionBarDeckSpinner(col: Collection, actionBar: ActionBar) { + fun initializeActionBarDeckSpinner( + col: Collection, + actionBar: ActionBar, + ) { actionBar.setDisplayShowTitleEnabled(false) // Add drop-down menu to select deck to action bar. @@ -96,58 +98,68 @@ class DeckSpinnerSelection( // custom implementation as DeckDropDownAdapter automatically includes a ALL_DECKS entry + // in order for the spinner to wrap the content a row layout with wrap_content for root // width was introduced - spinner.adapter = object : ArrayAdapter( - context, - R.layout.item_stats_deck, - it - ) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val rowView = super.getView(position, convertView, parent) - rowView.findViewById(R.id.title).text = getItem(position)!!.name - return rowView - } - }.apply { setDropDownViewResource(android.R.layout.simple_spinner_item) } + spinner.adapter = + object : ArrayAdapter( + context, + R.layout.item_stats_deck, + it, + ) { + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { + val rowView = super.getView(position, convertView, parent) + rowView.findViewById(R.id.title).text = getItem(position)!!.name + return rowView + } + }.apply { setDropDownViewResource(android.R.layout.simple_spinner_item) } setSpinnerListener() } } @MainThread // spinner.adapter - fun initializeNoteEditorDeckSpinner(col: Collection, @LayoutRes layoutResource: Int = R.layout.multiline_spinner_item) { + fun initializeNoteEditorDeckSpinner( + col: Collection, + @LayoutRes layoutResource: Int = R.layout.multiline_spinner_item, + ) { computeDropDownDecks(col, includeFiltered = false).toMutableList().let { dropDownDecks = it val deckNames = it.map { it.name } - val noteDeckAdapter: ArrayAdapter = object : - ArrayAdapter(context, layoutResource, 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 + val noteDeckAdapter: ArrayAdapter = + object : + ArrayAdapter(context, layoutResource, 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)) - } + // 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() } } /** @return All decks. */ - suspend fun computeDropDownDecks(includeFiltered: Boolean): List = - withCol { computeDropDownDecks(this, includeFiltered) } + suspend fun computeDropDownDecks(includeFiltered: Boolean): List = withCol { computeDropDownDecks(this, includeFiltered) } /** @return All decks. */ - private fun computeDropDownDecks(col: Collection, includeFiltered: Boolean): List = - col.decks.allNamesAndIds(includeFiltered = includeFiltered) + private fun computeDropDownDecks( + col: Collection, + includeFiltered: Boolean, + ): List = col.decks.allNamesAndIds(includeFiltered = includeFiltered) private fun setSpinnerListener() { spinner.setOnTouchListener { _: View?, motionEvent: MotionEvent -> @@ -195,13 +207,15 @@ class DeckSpinnerSelection( * the current deck id of Collection. * @return True if selection succeeded. */ - suspend fun selectDeckById(deckId: DeckId, setAsCurrentDeck: Boolean): Boolean { - return if (deckId == ALL_DECKS_ID || this.dropDownDecks == null) { + suspend fun selectDeckById( + deckId: DeckId, + setAsCurrentDeck: Boolean, + ): Boolean = + if (deckId == ALL_DECKS_ID || this.dropDownDecks == null) { selectAllDecks() } else { selectDeck(deckId, setAsCurrentDeck) } - } /** * select in the spinner deck with id @@ -209,7 +223,10 @@ class DeckSpinnerSelection( * @param setAsCurrentDeck whether this deck should be selected in the collection (if it exists) * @return whether it was found */ - private suspend fun selectDeck(deckId: DeckId, setAsCurrentDeck: Boolean): Boolean { + private suspend fun selectDeck( + deckId: DeckId, + setAsCurrentDeck: Boolean, + ): Boolean { val deck = this.dropDownDecks?.withIndex()?.firstOrNull { it.value.id == deckId } ?: return false val position = if (showAllDecks) deck.index + 1 else deck.index spinner.setSelection(position) @@ -225,7 +242,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/DeckUtils.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckUtils.kt index 87d599c30a0e..1afd9912f8b0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckUtils.kt @@ -27,7 +27,10 @@ import com.ichi2.libanki.Consts * @param includeSubdecks If true, includes subdecks in the check. Default is true. * @return `true` if the deck (and subdecks if specified) is empty, otherwise `false`. */ -private fun Collection.isDeckEmpty(deckId: Long, includeSubdecks: Boolean = true): Boolean { +private fun Collection.isDeckEmpty( + deckId: Long, + includeSubdecks: Boolean = true, +): Boolean { val deckIds = decks.deckAndChildIds(deckId) val totalCardCount = decks.cardCount(*deckIds.toLongArray(), includeSubdecks = includeSubdecks) return totalCardCount == 0 diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt index 89dfd49b9876..df5ecc8d3271 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DrawingActivity.kt @@ -62,8 +62,8 @@ class DrawingActivity : AnkiActivity() { whiteboardEditItem, ContextCompat.getColorStateList( this, - R.color.white - ) + R.color.white, + ), ) // undo button diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Ease.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Ease.kt index 5c84d041dcfc..84f7fdaf4cf4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Ease.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Ease.kt @@ -21,11 +21,14 @@ package com.ichi2.anki * @param value The so called value of the button. For the sake of consistency with upstream and our API * the buttons are numbered from 1 to 4. */ -enum class Ease(val value: Int) { +enum class Ease( + val value: Int, +) { AGAIN(1), HARD(2), GOOD(3), - EASY(4); + EASY(4), + ; companion object { fun fromValue(value: Int) = entries.first { value == it.value } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt index 1472d156695a..23dd2db691d7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditLine.kt @@ -91,16 +91,17 @@ class FieldEditLine : FrameLayout { } private fun toggleExpansionState() { - expansionState = when (expansionState) { - ExpansionState.EXPANDED -> { - collapseView(editText, enableAnimation) - ExpansionState.COLLAPSED - } - ExpansionState.COLLAPSED -> { - expandView(editText, enableAnimation) - ExpansionState.EXPANDED + expansionState = + when (expansionState) { + ExpansionState.EXPANDED -> { + collapseView(editText, enableAnimation) + ExpansionState.COLLAPSED + } + ExpansionState.COLLAPSED -> { + expandView(editText, enableAnimation) + ExpansionState.EXPANDED + } } - } setExpanderBackgroundImage() } @@ -111,9 +112,9 @@ class FieldEditLine : FrameLayout { } } - private fun getBackgroundImage(@DrawableRes idRes: Int): Drawable? { - return VectorDrawableCompat.create(this.resources, idRes, context.theme) - } + private fun getBackgroundImage( + @DrawableRes idRes: Int, + ): Drawable? = VectorDrawableCompat.create(this.resources, idRes, context.theme) fun setActionModeCallbacks(callback: ActionMode.Callback?) { editText.customSelectionActionModeCallback = callback @@ -132,7 +133,10 @@ class FieldEditLine : FrameLayout { } } - fun setContent(content: String?, replaceNewline: Boolean) { + fun setContent( + content: String?, + replaceNewline: Boolean, + ) { editText.setContent(content, replaceNewline) } @@ -222,7 +226,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) @@ -238,33 +245,33 @@ 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 = 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 = 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 fa32dba90124..39e390776131 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FieldEditText.kt @@ -44,7 +44,9 @@ import java.util.Locale import kotlin.math.max import kotlin.math.min -class FieldEditText : FixedEditText, NoteService.NoteField { +class FieldEditText : + FixedEditText, + NoteService.NoteField { override var ord = 0 private var origBackground: Drawable? = null private var selectionChangeListener: TextSelectionListener? = null @@ -65,9 +67,7 @@ class FieldEditText : FixedEditText, NoteService.NoteField { } } - private fun shouldDisableExtendedTextUi(): Boolean { - return this.context.sharedPrefs().getBoolean("disableExtendedTextUi", false) - } + private fun shouldDisableExtendedTextUi(): Boolean = this.context.sharedPrefs().getBoolean("disableExtendedTextUi", false) @KotlinCleanup("Simplify") override val fieldText: String? @@ -93,7 +93,10 @@ class FieldEditText : FixedEditText, NoteService.NoteField { this.pasteListener = pasteListener } - override fun onSelectionChanged(selStart: Int, selEnd: Int) { + override fun onSelectionChanged( + selStart: Int, + selEnd: Int, + ) { if (selectionChangeListener != null) { try { selectionChangeListener!!.onSelectionChanged(selStart, selEnd) @@ -123,14 +126,18 @@ class FieldEditText : FixedEditText, NoteService.NoteField { background = origBackground } - 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) } @@ -157,7 +164,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 @@ -165,8 +172,11 @@ class FieldEditText : FixedEditText, NoteService.NoteField { return false } - private fun onPaste(mediaUri: Uri?, description: ClipDescription?): Boolean { - return if (mediaUri == null) { + private fun onPaste( + mediaUri: Uri?, + description: ClipDescription?, + ): Boolean = + if (mediaUri == null) { false } else { try { @@ -177,7 +187,6 @@ class FieldEditText : FixedEditText, NoteService.NoteField { false } } - } override fun onRestoreInstanceState(state: Parcelable) { if (state !is SavedState) { @@ -190,25 +199,36 @@ 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 get() = this.inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES == InputType.TYPE_TEXT_FLAG_CAP_SENTENCES @Parcelize - internal class SavedState(val state: Parcelable?, val ord: Int) : BaseSavedState(state) + 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 PasteListener { - fun onPaste(editText: EditText, uri: Uri?, description: ClipDescription?): Boolean + fun onPaste( + editText: EditText, + uri: Uri?, + description: ClipDescription?, + ): 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 02b70ea6b37b..72ddd31dbdf4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FilteredDeckOptions.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FilteredDeckOptions.kt @@ -47,19 +47,21 @@ class FilteredDeckOptions : private var allowCommit = true // TODO: not anymore used in libanki? - private val dynExamples = 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 dynExamples = + 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) @@ -190,9 +192,7 @@ class FilteredDeckOptions : } } - override fun edit(): Editor { - return Editor() - } + override fun edit(): Editor = Editor() init { cacheValues() @@ -236,12 +236,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 @@ -296,16 +297,17 @@ class FilteredDeckOptions : val keys: Set = pref.values.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 @@ -347,25 +349,26 @@ 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 - } } @Suppress("deprecation") @@ -373,12 +376,13 @@ class FilteredDeckOptions : val reschedPref = findPreference(getString(R.string.filtered_deck_resched_key)) as CheckBoxPreference val delaysPrefCategory = findPreference(getString(R.string.filtered_deck_previewDelays_key)) as PreferenceCategory delaysPrefCategory.isEnabled = !reschedPref.isChecked - reschedPref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> - if (newValue !is Boolean) { - return@OnPreferenceChangeListener true + reschedPref.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> + if (newValue !is Boolean) { + return@OnPreferenceChangeListener true + } + delaysPrefCategory.isEnabled = !newValue + true } - delaysPrefCategory.isEnabled = !newValue - true - } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt index ff6a68dfb02b..996a76041da9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt @@ -41,7 +41,7 @@ enum class Flag( * Flag drawn to represents this flagInTheReviewer if it differs from [drawableRes]. * @TODO: Checks whether we can use colorControlNormal everywhere. */ - @DrawableRes val drawableReviewerRes: Int? = null + @DrawableRes val drawableReviewerRes: Int? = null, ) { NONE(0, R.id.flag_none, R.drawable.ic_flag_lightgrey, null, R.drawable.ic_flag_transparent), RED(1, R.id.flag_red, R.drawable.ic_flag_red, R.color.flag_red), @@ -49,7 +49,7 @@ enum class Flag( 2, R.id.flag_orange, R.drawable.ic_flag_orange, - R.color.flag_orange + R.color.flag_orange, ), GREEN(3, R.id.flag_green, R.drawable.ic_flag_green, R.color.flag_green), BLUE(4, R.id.flag_blue, R.drawable.ic_flag_blue, R.color.flag_blue), @@ -58,14 +58,15 @@ enum class Flag( 6, R.id.flag_turquoise, R.drawable.ic_flag_turquoise, - R.color.flag_turquoise + R.color.flag_turquoise, ), PURPLE( 7, R.id.flag_purple, R.drawable.ic_flag_purple, - R.color.flag_purple - ); + R.color.flag_purple, + ), + ; /** * Flag drawn to represents this flagInTheReviewer. @@ -83,16 +84,17 @@ enum class Flag( return labels.getLabel(this) ?: defaultDisplayName() } - private fun defaultDisplayName(): String = when (this) { - NONE -> TR.browsingNoFlag() - RED -> TR.actionsFlagRed() - ORANGE -> TR.actionsFlagOrange() - GREEN -> TR.actionsFlagGreen() - BLUE -> TR.actionsFlagBlue() - PINK -> TR.actionsFlagPink() - TURQUOISE -> TR.actionsFlagTurquoise() - PURPLE -> TR.actionsFlagPurple() - } + private fun defaultDisplayName(): String = + when (this) { + NONE -> TR.browsingNoFlag() + RED -> TR.actionsFlagRed() + ORANGE -> TR.actionsFlagOrange() + GREEN -> TR.actionsFlagGreen() + BLUE -> TR.actionsFlagBlue() + PINK -> TR.actionsFlagPink() + TURQUOISE -> TR.actionsFlagTurquoise() + PURPLE -> TR.actionsFlagPurple() + } /** * Renames the flag @@ -124,13 +126,19 @@ enum class Flag( * [Flag.NONE] does not have a label */ @JvmInline -private value class FlagLabels(val value: JSONObject) { +private value class FlagLabels( + val value: JSONObject, +) { /** * @return the user-defined label for the provided flag, or null if undefined * This is not supported for [Flag.NONE] and is validated outside this method */ fun getLabel(flag: Flag): String? = value.getStringOrNull(flag.code.toString()) - suspend fun updateName(flag: Flag, newName: String) { + + suspend fun updateName( + flag: Flag, + newName: String, + ) { value.put(flag.code.toString(), newName) withCol { config.set("flagLabels", value) @@ -138,7 +146,6 @@ private value class FlagLabels(val value: JSONObject) { } companion object { - suspend fun loadFromColConfig() = - FlagLabels(withCol { config.getObject("flagLabels", JSONObject()) }) + suspend fun loadFromColConfig() = FlagLabels(withCol { config.getObject("flagLabels", JSONObject()) }) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt b/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt index f1611a7583b2..c39cfa738bd4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/FlagToDisplay.kt @@ -18,14 +18,12 @@ package com.ichi2.anki class FlagToDisplay( private val actualFlag: Flag, private val isOnAppBar: Boolean, - private val isFullscreen: Boolean + private val isFullscreen: Boolean, ) { - - fun get(): Flag { - return when { + fun get(): Flag = + when { !isOnAppBar -> actualFlag isFullscreen -> actualFlag else -> Flag.NONE } - } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ImageOcclusionIntentBuilder.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ImageOcclusionIntentBuilder.kt index e202b67de44c..154a408358a1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ImageOcclusionIntentBuilder.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ImageOcclusionIntentBuilder.kt @@ -20,12 +20,12 @@ import android.content.Context import android.content.Intent import android.net.Uri import com.ichi2.anki.noteeditor.NoteEditorLauncher + /** * Builder class for creating intents related to image occlusion in the [NoteEditor]. */ -class ImageOcclusionIntentBuilder(private val context: Context) { - - fun buildIntent(imageUri: Uri?): Intent { - return NoteEditorLauncher.ImageOcclusion(imageUri).getIntent(context) - } +class ImageOcclusionIntentBuilder( + private val context: Context, +) { + fun buildIntent(imageUri: Uri?): Intent = NoteEditorLauncher.ImageOcclusion(imageUri).getIntent(context) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt index e1a1ef8244bd..76f591cf8070 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Import.kt @@ -73,18 +73,22 @@ fun Activity.onSelectedCsvForImport(data: Intent) { stackBuilder.startActivities() } -fun AnkiActivity.showImportDialog(id: Int, importPath: String) { +fun AnkiActivity.showImportDialog( + id: Int, + importPath: String, +) { Timber.d("showImportDialog() delegating to ImportDialog") val newFragment: AsyncDialogFragment = ImportDialog.newInstance(id, importPath) showAsyncDialogFragment(newFragment) } + fun AnkiActivity.showImportDialog() { showImportDialog( ImportOptions( importApkg = true, importColpkg = true, - importTextFile = true - ) + importTextFile = true, + ), ) } @@ -92,7 +96,10 @@ fun AnkiActivity.showImportDialog(options: ImportOptions) { showDialogFragment(ImportFileSelectionFragment.newInstance(options)) } -class DatabaseRestorationListener(val activity: AnkiActivity, val newAnkiDroidDirectory: String) : ImportColpkgListener { +class DatabaseRestorationListener( + val activity: AnkiActivity, + val newAnkiDroidDirectory: String, +) : ImportColpkgListener { override fun onImportColpkg(colpkgPath: String?) { Timber.i("Database restoration correct") activity.sharedPrefs().edit { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt index ab1ea659e8a5..8133c65df0da 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Info.kt @@ -45,7 +45,9 @@ private const val CHANGE_LOG_URL = "https://docs.ankidroid.org/changelog.html" /** * Shows an about box, which is a small HTML page. */ -class Info : AnkiActivity(), BaseSnackbarBuilderProvider { +class Info : + AnkiActivity(), + BaseSnackbarBuilderProvider { private lateinit var webView: WebView override val baseSnackbarBuilder: SnackbarBuilder = { @@ -68,35 +70,42 @@ class Info : AnkiActivity(), BaseSnackbarBuilderProvider { 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" webView = findViewById(R.id.info) - webView.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 + webView.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 (webView.canGoBack()) webView.goBack() + val onBackPressedCallback = + object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + if (webView.canGoBack()) webView.goBack() + } } - } // Apply Theme colors val typedArray = theme.obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground, android.R.attr.textColor)) val backgroundColor = typedArray.getColor(0, -1) @@ -118,43 +127,49 @@ class Info : AnkiActivity(), BaseSnackbarBuilderProvider { val background = backgroundColor.toRGBHex() webView.loadUrl("/android_asset/changelog.html") webView.settings.javaScriptEnabled = true - webView.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView, url: String) { + webView.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. - */ - webView.loadUrl( - "javascript:document.body.style.setProperty(\"color\", \"" + textColor + "\");" + - "x=document.getElementsByTagName(\"a\"); for(i=0;i finish() } @@ -166,14 +181,13 @@ class Info : AnkiActivity(), BaseSnackbarBuilderProvider { finishWithAnimation() } - private fun canOpenMarketUri(): Boolean { - return try { + private fun canOpenMarketUri(): Boolean = + try { canOpenIntent(this, AnkiDroidApp.getMarketIntent(this)) } catch (e: Exception) { Timber.w(e) false } - } private fun finishWithAnimation() { finish() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt index 4f02cdf6657e..6be340b8c21f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt @@ -50,28 +50,29 @@ object InitialActivity { return StartupFailure.WebviewFailed } - val failure = try { - CollectionManager.getColUnsafe() - return null - } catch (e: BackendException.BackendDbException.BackendDbLockedException) { - Timber.w(e) - StartupFailure.DatabaseLocked - } catch (e: BackendException.BackendDbException.BackendDbFileTooNewException) { - Timber.w(e) - StartupFailure.FutureAnkidroidVersion - } catch (e: SQLiteFullException) { - Timber.w(e) - StartupFailure.DiskFull - } catch (e: StorageAccessException) { - // Same handling as the fall through, but without the exception report - // These are now handled with a dialog and don't generate actionable reports - Timber.w(e) - StartupFailure.DBError(e) - } catch (e: Exception) { - Timber.w(e) - CrashReportService.sendExceptionReport(e, "InitialActivity::getStartupFailureType") - StartupFailure.DBError(e) - } + val failure = + try { + CollectionManager.getColUnsafe() + return null + } catch (e: BackendException.BackendDbException.BackendDbLockedException) { + Timber.w(e) + StartupFailure.DatabaseLocked + } catch (e: BackendException.BackendDbException.BackendDbFileTooNewException) { + Timber.w(e) + StartupFailure.FutureAnkidroidVersion + } catch (e: SQLiteFullException) { + Timber.w(e) + StartupFailure.DiskFull + } catch (e: StorageAccessException) { + // Same handling as the fall through, but without the exception report + // These are now handled with a dialog and don't generate actionable reports + Timber.w(e) + StartupFailure.DBError(e) + } catch (e: Exception) { + Timber.w(e) + CrashReportService.sendExceptionReport(e, "InitialActivity::getStartupFailureType") + StartupFailure.DBError(e) + } if (!AnkiDroidApp.isSdCardMounted) { return StartupFailure.SDCardNotMounted @@ -84,9 +85,10 @@ object InitialActivity { /** @return Whether any preferences were upgraded */ - fun upgradePreferences(context: Context, previousVersionCode: Long): Boolean { - return PreferenceUpgradeService.upgradePreferences(context, previousVersionCode) - } + fun upgradePreferences( + context: Context, + previousVersionCode: Long, + ): Boolean = PreferenceUpgradeService.upgradePreferences(context, previousVersionCode) /** * @return Whether a fresh install occurred and a "fresh install" setup for preferences was performed @@ -117,8 +119,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) { @@ -131,22 +132,30 @@ object InitialActivity { * This is not called in the case of performSetupFromFreshInstall returning true. * So this should not use the default value */ - fun isLatestVersion(preferences: SharedPreferences): Boolean { - return preferences.getString("lastVersion", "") == pkgVersionName - } + fun isLatestVersion(preferences: SharedPreferences): Boolean = preferences.getString("lastVersion", "") == pkgVersionName sealed class StartupFailure { object SDCardNotMounted : StartupFailure() + object DirectoryNotAccessible : StartupFailure() + object FutureAnkidroidVersion : StartupFailure() - class DBError(val exception: Exception) : StartupFailure() + + class DBError( + val exception: Exception, + ) : StartupFailure() + object DatabaseLocked : StartupFailure() + object WebviewFailed : StartupFailure() + object DiskFull : StartupFailure() } } -sealed class AnkiDroidFolder(val permissionSet: PermissionSet) { +sealed class AnkiDroidFolder( + val permissionSet: PermissionSet, +) { /** * AnkiDroid will use the folder ~/AnkiDroid by default * To access it, we must first get [permissionSet].permissions. @@ -154,7 +163,9 @@ sealed class AnkiDroidFolder(val permissionSet: PermissionSet) { * but increase the risk of space used on their storage when they don't want to. * It can not be used on the play store starting with Sdk 30. **/ - class PublicFolder(requiredPermissions: PermissionSet) : AnkiDroidFolder(requiredPermissions) + class PublicFolder( + requiredPermissions: PermissionSet, + ) : AnkiDroidFolder(requiredPermissions) /** * AnkiDroid will use the app-private folder: `~/Android/data/com.ichi2.anki[.A]/files/AnkiDroid`. @@ -164,13 +175,14 @@ sealed class AnkiDroidFolder(val permissionSet: PermissionSet) { */ data object AppPrivateFolder : AnkiDroidFolder(PermissionSet.APP_PRIVATE) - fun hasRequiredPermissions(context: Context): Boolean { - return Permissions.hasAllPermissions(context, permissionSet.permissions) - } + fun hasRequiredPermissions(context: Context): Boolean = Permissions.hasAllPermissions(context, permissionSet.permissions) } @Parcelize -enum class PermissionSet(val permissions: List, val permissionsFragment: Class?) : Parcelable { +enum class PermissionSet( + val permissions: List, + val permissionsFragment: Class?, +) : Parcelable { LEGACY_ACCESS(Permissions.legacyStorageAccessPermissions, PermissionsUntil29Fragment::class.java), @RequiresApi(Build.VERSION_CODES.R) @@ -179,10 +191,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), } /** @@ -193,7 +205,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, @@ -221,6 +233,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 686ea1344ce2..1908de4dfe0d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt @@ -75,18 +75,21 @@ class IntentHandler : AbstractIntentHandler() { SyncWorker.cancel(this) } when (launchType) { - LaunchType.FILE_IMPORT -> runIfStoragePermissions { - handleFileImport(fileIntent, reloadIntent, action) - finish() - } - LaunchType.TEXT_IMPORT -> runIfStoragePermissions { - onSelectedCsvForImport(fileIntent) - finish() - } - LaunchType.IMAGE_IMPORT -> runIfStoragePermissions { - handleImageImport(intent) - finish() - } + LaunchType.FILE_IMPORT -> + runIfStoragePermissions { + handleFileImport(fileIntent, reloadIntent, action) + finish() + } + LaunchType.TEXT_IMPORT -> + runIfStoragePermissions { + onSelectedCsvForImport(fileIntent) + finish() + } + LaunchType.IMAGE_IMPORT -> + runIfStoragePermissions { + handleImageImport(intent) + finish() + } LaunchType.SYNC -> runIfStoragePermissions { handleSyncIntent(reloadIntent, action) } LaunchType.REVIEW -> runIfStoragePermissions { handleReviewIntent(intent) } LaunchType.DEFAULT_START_APP_IF_NEW -> { @@ -105,7 +108,7 @@ class IntentHandler : AbstractIntentHandler() { // null string is handled by copyToClipboard in try-catch this.copyToClipboard( text = (intent.getStringExtra(CLIPBOARD_INTENT_EXTRA_DATA)!!), - failureMessageId = R.string.about_ankidroid_error_copy_debug_info + failureMessageId = R.string.about_ankidroid_error_copy_debug_info, ) } @@ -127,7 +130,11 @@ class IntentHandler : AbstractIntentHandler() { * */ @NeedsTest("clicking a file in 'Files' to import") - private fun performActionIfStorageAccessible(reloadIntent: Intent, action: String?, block: () -> Unit) { + private fun performActionIfStorageAccessible( + reloadIntent: Intent, + action: String?, + block: () -> Unit, + ) { if (grantedStoragePermissions(this, showToast = true)) { Timber.i("User has storage permissions. Running intent: %s", action) block() @@ -140,17 +147,21 @@ class IntentHandler : AbstractIntentHandler() { private fun handleReviewIntent(intent: Intent) { val deckId = intent.getLongExtra(ReminderService.EXTRA_DECK_ID, 0) Timber.i("Handling intent to review deck '%d'", deckId) - val reviewIntent = if (sharedPrefs().getBoolean("newReviewer", false)) { - ReviewerFragment.getIntent(this) - } else { - Intent(this, Reviewer::class.java) - } + val reviewIntent = + if (sharedPrefs().getBoolean("newReviewer", false)) { + ReviewerFragment.getIntent(this) + } else { + Intent(this, Reviewer::class.java) + } CollectionManager.getColUnsafe().decks.select(deckId) startActivity(reviewIntent) finish() } - private fun handleSyncIntent(reloadIntent: Intent, action: String?) { + private fun handleSyncIntent( + reloadIntent: Intent, + action: String?, + ) { Timber.i("Handling Sync Intent") sendDoSyncMsg() reloadIntent.action = action @@ -159,7 +170,11 @@ class IntentHandler : AbstractIntentHandler() { finish() } - private fun handleFileImport(intent: Intent, reloadIntent: Intent, action: String?) { + private fun handleFileImport( + intent: Intent, + reloadIntent: Intent, + action: String?, + ) { Timber.i("Handling file import") if (!hasShownAppIntro()) { Timber.i("Trying to import a file when the app was not started at all") @@ -189,13 +204,14 @@ class IntentHandler : AbstractIntentHandler() { private fun deleteImportedDeck(path: String?) { try { val file = File(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") @@ -205,16 +221,18 @@ class IntentHandler : AbstractIntentHandler() { } private fun handleImageImport(data: Intent) { - val imageUri = if (intent.action == Intent.ACTION_SEND) { - IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java) - } else { - data.data - } + val imageUri = + if (intent.action == Intent.ACTION_SEND) { + IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java) + } else { + data.data + } val imageOcclusionIntentBuilder = ImageOcclusionIntentBuilder(this) val intentImageOcclusion = imageOcclusionIntentBuilder.buildIntent(imageUri) - TaskStackBuilder.create(this) + TaskStackBuilder + .create(this) .addNextIntentWithParentStack(Intent(this, DeckPicker::class.java)) .addNextIntent(intentImageOcclusion) .startActivities() @@ -260,7 +278,9 @@ class IntentHandler : AbstractIntentHandler() { /** image */ IMAGE_IMPORT, - SYNC, REVIEW, COPY_DEBUG_INFO + SYNC, + REVIEW, + COPY_DEBUG_INFO, } companion object { @@ -277,8 +297,14 @@ class IntentHandler : AbstractIntentHandler() { * it verifies if the app has been granted the necessary storage access permission. * @return `true`: if granted, otherwise `false` and shows a missing permission toast */ - fun grantedStoragePermissions(context: Context, showToast: Boolean): Boolean { - val granted = !ScopedStorageService.isLegacyStorage(context) || hasLegacyStorageAccessPermission(context) || Permissions.isExternalStorageManagerCompat() + fun grantedStoragePermissions( + context: Context, + showToast: Boolean, + ): Boolean { + val granted = + !ScopedStorageService.isLegacyStorage(context) || + hasLegacyStorageAccessPermission(context) || + Permissions.isExternalStorageManagerCompat() if (!granted && showToast) { showThemedToast(context, context.getString(R.string.intent_handler_failed_no_storage_permission), false) @@ -295,7 +321,8 @@ class IntentHandler : AbstractIntentHandler() { val mimeType = intent.resolveMimeType() when { mimeType?.startsWith("image/") == true -> LaunchType.IMAGE_IMPORT - mimeType == "text/tab-separated-values" || mimeType == "text/comma-separated-values" -> LaunchType.TEXT_IMPORT + mimeType == "text/tab-separated-values" || + mimeType == "text/comma-separated-values" -> LaunchType.TEXT_IMPORT else -> LaunchType.FILE_IMPORT } } else if ("com.ichi2.anki.DO_SYNC" == action) { @@ -317,30 +344,33 @@ class IntentHandler : AbstractIntentHandler() { 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)) + } - fun requiresCollectionAccess(launchType: LaunchType): Boolean { - return when (launchType) { + fun requiresCollectionAccess(launchType: LaunchType): Boolean = + when (launchType) { LaunchType.SYNC, LaunchType.REVIEW, LaunchType.DEFAULT_START_APP_IF_NEW, LaunchType.FILE_IMPORT, LaunchType.TEXT_IMPORT, - LaunchType.IMAGE_IMPORT -> true + LaunchType.IMAGE_IMPORT, + -> true LaunchType.COPY_DEBUG_INFO -> false } - } - class DoSync : DialogHandlerMessage( - which = WhichDialogHandler.MSG_DO_SYNC, - analyticName = "DoSyncDialog" - ) { + class DoSync : + DialogHandlerMessage( + which = WhichDialogHandler.MSG_DO_SYNC, + analyticName = "DoSyncDialog", + ) { override fun handleAsyncMessage(activity: AnkiActivity) { // we may be called via any AnkiActivity but sync is a DeckPicker thing if (activity !is DeckPicker) { @@ -348,7 +378,7 @@ class IntentHandler : AbstractIntentHandler() { activity, activity.getString(R.string.something_wrong), ClassCastException(activity.javaClass.simpleName + " is not " + DeckPicker.javaClass.simpleName), - true + true, ) return } @@ -369,17 +399,18 @@ class IntentHandler : AbstractIntentHandler() { 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, ) } } @@ -389,8 +420,9 @@ class IntentHandler : AbstractIntentHandler() { 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() } } @@ -401,11 +433,13 @@ class IntentHandler : AbstractIntentHandler() { * legacy or the new reviewer based on the "newReviewer" preference. * It is expected to be used from widget, shortcut, reminders but not from ankidroid directly because of the CLEAR_TOP flag. */ - fun intentToReviewDeckFromShorcuts(context: Context, deckId: DeckId) = - Intent(context, IntentHandler::class.java).apply { - setAction(Intent.ACTION_VIEW) - putExtra(ReminderService.EXTRA_DECK_ID, deckId) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - } + fun intentToReviewDeckFromShorcuts( + context: Context, + deckId: DeckId, + ) = Intent(context, IntentHandler::class.java).apply { + setAction(Intent.ACTION_VIEW) + putExtra(ReminderService.EXTRA_DECK_ID, deckId) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt index 9b11408b2e11..061d375d9b60 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/IntroductionActivity.kt @@ -40,16 +40,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)) { @@ -96,6 +96,4 @@ class IntroductionActivity : AnkiActivity() { } } -internal fun Context.hasShownAppIntro(): Boolean { - return sharedPrefs().getBoolean(IntroductionActivity.INTRODUCTION_SLIDES_SHOWN, false) -} +internal fun Context.hasShownAppIntro(): Boolean = sharedPrefs().getBoolean(IntroductionActivity.INTRODUCTION_SLIDES_SHOWN, false) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptSTT.kt b/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptSTT.kt index 373980f3a5d3..377f8e0ce9b1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptSTT.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptSTT.kt @@ -24,13 +24,16 @@ import android.speech.RecognizerIntent import android.speech.SpeechRecognizer import com.ichi2.utils.Permissions.canRecordAudio -class JavaScriptSTT(private val context: Context) { +class JavaScriptSTT( + private val context: Context, +) { private var speechRecognizer: SpeechRecognizer? = null private var recognitionCallback: SpeechRecognitionCallback? = null private var language: String? = null interface SpeechRecognitionCallback { fun onResult(results: List) + fun onError(errorMessage: String) } @@ -75,8 +78,8 @@ class JavaScriptSTT(private val context: Context) { return true } - private fun createRecognitionListener(): RecognitionListener { - return object : RecognitionListener { + private fun createRecognitionListener(): RecognitionListener = + object : RecognitionListener { override fun onReadyForSpeech(params: Bundle?) {} override fun onBeginningOfSpeech() {} @@ -88,18 +91,19 @@ class JavaScriptSTT(private val context: Context) { override fun onEndOfSpeech() {} override fun onError(error: Int) { - val errorMessage = when (error) { - SpeechRecognizer.ERROR_AUDIO -> "Audio error" - SpeechRecognizer.ERROR_CLIENT -> "Client error" - SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "Insufficient permissions" - SpeechRecognizer.ERROR_NETWORK -> "Network error" - SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> "Network timeout" - SpeechRecognizer.ERROR_NO_MATCH -> "No match found" - SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> "Recognition service busy" - SpeechRecognizer.ERROR_SERVER -> "Server error" - SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "Speech timeout" - else -> "Unknown error" - } + val errorMessage = + when (error) { + SpeechRecognizer.ERROR_AUDIO -> "Audio error" + SpeechRecognizer.ERROR_CLIENT -> "Client error" + SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "Insufficient permissions" + SpeechRecognizer.ERROR_NETWORK -> "Network error" + SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> "Network timeout" + SpeechRecognizer.ERROR_NO_MATCH -> "No match found" + SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> "Recognition service busy" + SpeechRecognizer.ERROR_SERVER -> "Server error" + SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "Speech timeout" + else -> "Unknown error" + } recognitionCallback?.onError(errorMessage) } @@ -114,7 +118,9 @@ class JavaScriptSTT(private val context: Context) { override fun onPartialResults(partialResults: Bundle?) {} - override fun onEvent(eventType: Int, params: Bundle?) {} + override fun onEvent( + eventType: Int, + params: Bundle?, + ) {} } - } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptTTS.kt b/AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptTTS.kt index f86d0b02b845..6b78e6408fa7 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,9 +50,10 @@ class JavaScriptTTS internal constructor() : OnInitListener { * @return ERROR(-1) SUCCESS(0) */ @ErrorOrSuccess - fun speak(text: String?, @QueueMode queueMode: Int): Int { - return mTts.speak(text, queueMode, mTtsParams, "stringId") - } + fun speak( + text: String?, + @QueueMode queueMode: Int, + ): Int = mTts.speak(text, queueMode, mTtsParams, "stringId") /** * If only a string is given, set QUEUE_FLUSH to the default behavior. @@ -58,9 +61,7 @@ class JavaScriptTTS internal constructor() : OnInitListener { * @return ERROR(-1) SUCCESS(0) */ @ErrorOrSuccess - fun speak(text: String?): Int { - return mTts.speak(text, TextToSpeech.QUEUE_FLUSH, mTtsParams, "stringId") - } + fun speak(text: String?): Int = mTts.speak(text, TextToSpeech.QUEUE_FLUSH, mTtsParams, "stringId") /** * Sets the text-to-speech language. @@ -117,9 +118,7 @@ class JavaScriptTTS internal constructor() : OnInitListener { * @return ERROR(-1) SUCCESS(0) */ @ErrorOrSuccess - fun stop(): Int { - return mTts.stop() - } + fun stop(): Int = mTts.stop() companion object { private const val TTS_SUCCESS = TextToSpeech.SUCCESS 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 a1f3afe8521e..942683db1918 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/LoginActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/LoginActivity.kt @@ -44,8 +44,9 @@ import timber.log.Timber * TODO: Move this to a fragment */ @NeedsTest("14650: collection permissions are required for this screen to be usable") -class LoginActivity : MyAccount(), CollectionPermissionScreenLauncher { - +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 857cea32636b..da90ebf9bc82 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt @@ -41,7 +41,9 @@ import java.lang.IllegalStateException * this class is required in summer note class for paste image event and in visual editor activity for importing media, * (extracted code to avoid duplication of code). */ -class MediaRegistration(private val context: Context) { +class MediaRegistration( + private val context: Context, +) { // Use the same HTML if the same image is pasted multiple times. private val pastedMediaCache = HashMap() @@ -51,12 +53,17 @@ class MediaRegistration(private val context: Context) { * @return HTML referring to the loaded image */ @Throws(IOException::class) - fun loadMediaIntoCollection(uri: Uri, description: ClipDescription): String? { + fun loadMediaIntoCollection( + uri: Uri, + description: ClipDescription, + ): String? { val filename = getFileName(context.contentResolver, uri) val fd = openInputStreamWithURI(uri) - val (fileName, fileExtensionWithDot) = FileNameAndExtension.fromString(filename) - ?.renameForCreateTempFile() - ?: throw IllegalStateException("Unable to determine valid filename") + val (fileName, fileExtensionWithDot) = + FileNameAndExtension + .fromString(filename) + ?.renameForCreateTempFile() + ?: throw IllegalStateException("Unable to determine valid filename") var clipCopy: File var bytesWritten: Long val isImage = ClipboardUtil.hasImage(description) @@ -84,31 +91,31 @@ class MediaRegistration(private val context: Context) { Timber.d("File was %d bytes", bytesWritten) if (bytesWritten > MEDIA_MAX_SIZE) { Timber.w("File was too large: %d bytes", bytesWritten) - val message = if (isImage) { - context.getString(R.string.note_editor_image_too_large) - } else if (isVideo) { - context.getString(R.string.note_editor_video_too_large) - } else { - context.getString(R.string.note_editor_audio_too_large) - } + val message = + if (isImage) { + context.getString(R.string.note_editor_image_too_large) + } else if (isVideo) { + context.getString(R.string.note_editor_video_too_large) + } else { + context.getString(R.string.note_editor_audio_too_large) + } showThemedToast(context, message, false) File(tempFilePath).delete() return null } - val field = if (isImage) { - ImageField() - } else { - MediaClipField() - } + val field = + if (isImage) { + ImageField() + } else { + MediaClipField() + } field.hasTemporaryMedia = true field.mediaPath = tempFilePath return field.formattedValue } @Throws(FileNotFoundException::class) - private fun openInputStreamWithURI(uri: Uri): InputStream { - return context.contentResolver.openInputStream(uri)!! - } + private fun openInputStreamWithURI(uri: Uri): InputStream = context.contentResolver.openInputStream(uri)!! private fun convertToJPG(file: File): Boolean { val bm = BitmapFactory.decodeFile(file.absolutePath) @@ -126,7 +133,11 @@ class MediaRegistration(private val context: Context) { return true // successful conversion to jpg. } - private fun shouldConvertToJPG(fileNameExtension: String, fileStream: InputStream, isImage: Boolean): Boolean { + private fun shouldConvertToJPG( + fileNameExtension: String, + fileStream: InputStream, + isImage: Boolean, + ): Boolean { if (!isImage) { return false } @@ -145,8 +156,11 @@ class MediaRegistration(private val context: Context) { return true } - fun onPaste(uri: Uri, description: ClipDescription): String? { - return try { + fun onPaste( + uri: Uri, + description: ClipDescription, + ): String? = + try { // check if cache already holds registered file or not if (!pastedMediaCache.containsKey(uri.toString())) { pastedMediaCache[uri.toString()] = loadMediaIntoCollection(uri, description) @@ -171,7 +185,6 @@ class MediaRegistration(private val context: Context) { showThemedToast(context, context.getString(R.string.multimedia_editor_something_wrong), false) null } - } @CheckResult fun registerMediaForWebView(mediaPath: String?): Boolean { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/MetaDB.kt b/AnkiDroid/src/main/java/com/ichi2/anki/MetaDB.kt index edd3769f26af..03395ccb4e04 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,15 +72,15 @@ 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)", ) metaDb.execSQL( "CREATE TABLE IF NOT EXISTS micToolbarState (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "did INTEGER NOT NULL, state INTEGER NOT NULL)" + "did INTEGER NOT NULL, state INTEGER NOT NULL)", ) updateWidgetStatus(metaDb) @@ -91,7 +95,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 } @@ -123,7 +127,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)", ) } } @@ -210,7 +214,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: CardSide, language: String) { + fun storeLanguage( + context: Context, + did: DeckId, + ord: Int, + qa: CardSide, + language: String, + ) { openDBIfClosed(context) try { if ("" == getLanguage(context, did, ord, qa)) { @@ -220,8 +230,8 @@ object MetaDB { did, ord, qa.int, - language - ) + language, + ), ) Timber.v("Store language for deck %d", did) } else { @@ -231,8 +241,8 @@ object MetaDB { language, did, ord, - qa.int - ) + qa.int, + ), ) Timber.v("Update language for deck %d", did) } @@ -248,24 +258,30 @@ 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: CardSide): String { + fun getLanguage( + context: Context, + did: DeckId, + ord: Int, + qa: CardSide, + ): String { openDBIfClosed(context) var language = "" val query = "SELECT language FROM languages WHERE did = ? AND ord = ? AND qa = ? LIMIT 1" try { - mMetaDb!!.rawQuery( - query, - arrayOf( - did.toString(), - ord.toString(), - qa.int.toString() - ) - ).use { cur -> - Timber.v("getLanguage: %s", query) - if (cur.moveToNext()) { - language = cur.getString(0) + mMetaDb!! + .rawQuery( + query, + arrayOf( + did.toString(), + ord.toString(), + qa.int.toString(), + ), + ).use { cur -> + Timber.v("getLanguage: %s", query) + if (cur.moveToNext()) { + language = cur.getString(0) + } } - } } catch (e: Exception) { Timber.e(e, "Error fetching language ") } @@ -277,13 +293,17 @@ 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(did.toString()) - ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } + mMetaDb!! + .rawQuery( + "SELECT state FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } } catch (e: Exception) { Timber.e(e, "Error retrieving whiteboard state from MetaDB ") return false @@ -296,29 +316,34 @@ object MetaDB { * @param did deck id to store whiteboard state for * @param whiteboardState `true` if the whiteboard should be shown, `false` 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(did.toString()) - ).use { cur -> - if (cur.moveToNext()) { - metaDb.execSQL( - "UPDATE whiteboardState SET did = ?, state=? WHERE _id=?;", - 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) - ) - Timber.d("Store whiteboard state (%d) for deck %d", state, did) + metaDb + .rawQuery( + "SELECT _id FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> + if (cur.moveToNext()) { + metaDb.execSQL( + "UPDATE whiteboardState SET did = ?, state=? WHERE _id=?;", + 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), + ) + Timber.d("Store whiteboard state (%d) for deck %d", state, did) + } } - } } catch (e: Exception) { Timber.e(e, "Error storing whiteboard state in MetaDB ") } @@ -329,13 +354,17 @@ 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(did.toString()) - ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } + mMetaDb!! + .rawQuery( + "SELECT stylus FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } } catch (e: Exception) { Timber.e(e, "Error retrieving whiteboard stylus mode state from MetaDB ") return false @@ -348,29 +377,34 @@ 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(did.toString()) - ).use { cur -> - if (cur.moveToNext()) { - metaDb.execSQL( - "UPDATE whiteboardState SET did = ?, stylus=? WHERE _id=?;", - 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) - ) - Timber.d("Store whiteboard stylus mode state (%d) for deck %d", state, did) + metaDb + .rawQuery( + "SELECT _id FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> + if (cur.moveToNext()) { + metaDb.execSQL( + "UPDATE whiteboardState SET did = ?, stylus=? WHERE _id=?;", + 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), + ) + Timber.d("Store whiteboard stylus mode state (%d) for deck %d", state, did) + } } - } } catch (e: Exception) { Timber.e(e, "Error storing whiteboard stylus mode state in MetaDB ") } @@ -381,13 +415,17 @@ 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(did.toString()) - ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } + mMetaDb!! + .rawQuery( + "SELECT visible FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } } catch (e: Exception) { Timber.e(e, "Error retrieving whiteboard state from MetaDB ") return false @@ -400,29 +438,34 @@ object MetaDB { * @param did deck id to store whiteboard state for * @param isVisible `true` if the whiteboard should be shown, `false` 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(did.toString()) - ).use { cur -> - if (cur.moveToNext()) { - metaDb.execSQL( - "UPDATE whiteboardState SET did = ?, visible= ? WHERE _id=?;", - 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) - ) - Timber.d("Store whiteboard visibility (%d) for deck %d", isVisibleState, did) + metaDb + .rawQuery( + "SELECT _id FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> + if (cur.moveToNext()) { + metaDb.execSQL( + "UPDATE whiteboardState SET did = ?, visible= ? WHERE _id=?;", + 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), + ) + Timber.d("Store whiteboard visibility (%d) for deck %d", isVisibleState, did) + } } - } } catch (e: Exception) { Timber.e(e, "Error storing whiteboard visibility in MetaDB ") } @@ -431,18 +474,22 @@ 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(did.toString()) - ).use { cur -> - cur.moveToFirst() - val light = DatabaseUtil.getInteger(cur, 0) - val dark = DatabaseUtil.getInteger(cur, 1) - return WhiteboardPenColor(light, dark) - } + mMetaDb!! + .rawQuery( + "SELECT lightpencolor, darkpencolor FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> + cur.moveToFirst() + val light = DatabaseUtil.getInteger(cur, 0) + val dark = DatabaseUtil.getInteger(cur, 1) + return WhiteboardPenColor(light, dark) + } } catch (e: Exception) { Timber.e(e, "Error retrieving whiteboard pen color from MetaDB ") return default @@ -456,28 +503,34 @@ 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(did.toString()) - ).use { cur -> - if (cur.moveToNext()) { - metaDb.execSQL( - "UPDATE whiteboardState SET did = ?, " + - columnName + "= ? " + - " WHERE _id=?;", - arrayOf(did, value, cur.getString(0)) - ) - } else { - val sql = "INSERT INTO whiteboardState (did, $columnName) VALUES (?, ?)" - metaDb.execSQL(sql, arrayOf(did, value)) + metaDb + .rawQuery( + "SELECT _id FROM whiteboardState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> + if (cur.moveToNext()) { + metaDb.execSQL( + "UPDATE whiteboardState SET did = ?, " + + columnName + "= ? " + + " WHERE _id=?;", + arrayOf(did, value, cur.getString(0)), + ) + } else { + val sql = "INSERT INTO whiteboardState (did, $columnName) VALUES (?, ?)" + metaDb.execSQL(sql, arrayOf(did, value)) + } + Timber.d("Store whiteboard %s (%d) for deck %d", columnName, value, did) } - Timber.d("Store whiteboard %s (%d) for deck %d", columnName, value, did) - } } catch (e: Exception) { Timber.w(e, "Error storing whiteboard color in MetaDB") } @@ -488,13 +541,17 @@ object MetaDB { * * @return `true` if the toolbar should be shown, `false` otherwise */ - fun getMicToolbarState(context: Context, did: DeckId): Boolean { + fun getMicToolbarState( + context: Context, + did: DeckId, + ): Boolean { openDBIfClosed(context) try { - mMetaDb!!.rawQuery( - "SELECT state FROM micToolbarState WHERE did = ?", - arrayOf(did.toString()) - ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } + mMetaDb!! + .rawQuery( + "SELECT state FROM micToolbarState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> return DatabaseUtil.getScalarBoolean(cur) } } catch (e: Exception) { Timber.e(e, "Error retrieving micToolbar state from MetaDB ") return false @@ -507,28 +564,33 @@ object MetaDB { * @param did deck id to store mic toolbar state for * @param micToolbarState `true` if the toolbar should be shown, `false` otherwise */ - fun storeMicToolbarState(context: Context, did: DeckId, isEnabled: Boolean) { + fun storeMicToolbarState( + context: Context, + did: DeckId, + isEnabled: Boolean, + ) { val state = if (isEnabled) 1 else 0 openDBIfClosed(context) try { val metaDb = mMetaDb!! - metaDb.rawQuery( - "SELECT _id FROM micToolbarState WHERE did = ?", - arrayOf(did.toString()) - ).use { cur -> - if (cur.moveToNext()) { - metaDb.execSQL( - "UPDATE micToolbarState SET did = ?, state = ? WHERE _id = ?;", - arrayOf(did, state, cur.getString(0)) - ) - } else { - metaDb.execSQL( - "INSERT INTO micToolbarState (did, state) VALUES (?, ?)", - arrayOf(did, state) - ) + metaDb + .rawQuery( + "SELECT _id FROM micToolbarState WHERE did = ?", + arrayOf(did.toString()), + ).use { cur -> + if (cur.moveToNext()) { + metaDb.execSQL( + "UPDATE micToolbarState SET did = ?, state = ? WHERE _id = ?;", + arrayOf(did, state, cur.getString(0)), + ) + } else { + metaDb.execSQL( + "INSERT INTO micToolbarState (did, state) VALUES (?, ?)", + arrayOf(did, state), + ) + } + Timber.d("Store mic toolbar state (%d) for deck %d", state, did) } - Timber.d("Store mic toolbar state (%d) for deck %d", state, did) - } } catch (e: Exception) { Timber.e(e, "Error storing mic toolbar state in MetaDB ") } @@ -542,19 +604,20 @@ object MetaDB { fun getWidgetSmallStatus(context: Context): IntArray { openDBIfClosed(context) try { - mMetaDb!!.query( - "smallWidgetStatus", - arrayOf("due", "eta"), - null, - null, - null, - null, - null - ).use { cursor -> - if (cursor.moveToNext()) { - return intArrayOf(cursor.getInt(0), cursor.getInt(1)) + mMetaDb!! + .query( + "smallWidgetStatus", + arrayOf("due", "eta"), + null, + null, + null, + null, + null, + ).use { cursor -> + if (cursor.moveToNext()) { + return intArrayOf(cursor.getInt(0), cursor.getInt(1)) + } } - } } catch (e: SQLiteException) { Timber.e(e, "Error while querying widgetStatus") } @@ -576,7 +639,10 @@ object MetaDB { return due } - fun storeSmallWidgetStatus(context: Context, status: SmallWidgetStatus) { + fun storeSmallWidgetStatus( + context: Context, + status: SmallWidgetStatus, + ) { openDBIfClosed(context) try { val metaDb = mMetaDb!! @@ -586,7 +652,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 { @@ -612,23 +678,25 @@ object MetaDB { } private object DatabaseUtil { - fun getScalarBoolean(cur: Cursor): Boolean { - return if (cur.moveToNext()) { + fun getScalarBoolean(cur: Cursor): Boolean = + if (cur.moveToNext()) { cur.getInt(0) > 0 } else { false } - } // API LEVEL - fun getTableColumnCount(metaDb: SQLiteDatabase, tableName: String) = - metaDb.rawQuery("PRAGMA table_info($tableName)", null).use { c -> - c.count - } - - fun getInteger(cur: Cursor, columnIndex: Int): Int? { - return if (cur.isNull(columnIndex)) null else cur.getInt(columnIndex) + fun getTableColumnCount( + metaDb: SQLiteDatabase, + tableName: String, + ) = metaDb.rawQuery("PRAGMA table_info($tableName)", null).use { c -> + c.count } + + fun getInteger( + cur: Cursor, + columnIndex: Int, + ): Int? = if (cur.isNull(columnIndex)) null else cur.getInt(columnIndex) } private fun isDBOpen() = mMetaDb?.isOpen == true diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt index 0cac20c5d56a..ecbc96635c50 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ModelFieldEditor.kt @@ -55,7 +55,9 @@ import org.json.JSONException import timber.log.Timber import java.util.Locale -class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { +class ModelFieldEditor : + AnkiActivity(), + LocaleSelectionDialogHandler { // Position of the current field selected private var currentPos = 0 private lateinit var fieldsListView: ListView @@ -105,6 +107,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 @@ -122,22 +125,26 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { noteFields = notetype.getJSONArray("flds") fieldsLabels = noteFields.toStringList("name") fieldsListView.adapter = ArrayAdapter(this, R.layout.model_field_editor_list_item, fieldsLabels) - fieldsListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position: Int, _ -> - showDialogFragment(newInstance(fieldsLabels[position])) - currentPos = position - } + fieldsListView.onItemClickListener = + AdapterView.OnItemClickListener { _, _, position: Int, _ -> + showDialogFragment(newInstance(fieldsLabels[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) { @@ -159,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 { @@ -181,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) } @@ -204,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) { @@ -227,13 +239,14 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { * Creates a dialog to delete the currently selected field */ private fun deleteFieldDialog() { - val confirm = Runnable { - getColUnsafe.modSchemaNoCheck() - deleteField() + val confirm = + Runnable { + getColUnsafe.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 (fieldsLabels.size < 2) { showThemedToast(this, resources.getString(R.string.toast_last_field), true) @@ -260,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.remFieldLegacy(notetype, noteFields.getJSONObject(currentPos)) - true - } catch (e: ConfirmModSchemaException) { - // Should never be reached - e.log() - false + val result = + withCol { + try { + notetypes.remFieldLegacy(notetype, noteFields.getJSONObject(currentPos)) + true + } catch (e: ConfirmModSchemaException) { + // Should never be reached + e.log() + false + } } - } if (!result) { closeActivity() } @@ -304,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 { - getColUnsafe.modSchemaNoCheck() - try { - renameField() - } catch (e1: ConfirmModSchemaException) { - e1.log() - // This should never be thrown + val confirm = + Runnable { + getColUnsafe.modSchemaNoCheck() + try { + renameField() + } catch (e1: ConfirmModSchemaException) { + e1.log() + // This should never be thrown + } } - } c.setConfirm(confirm) this@ModelFieldEditor.showDialogFragment(c) } @@ -336,13 +351,14 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { title(text = String.format(resources.getString(R.string.model_field_editor_reposition), 1, fieldsLabels.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 > fieldsLabels.size) { fieldNameInput.error = resources.getString(R.string.toast_out_of_range) } else { @@ -356,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 { - getColUnsafe.modSchemaNoCheck() - repositionField(pos - 1) - } catch (e1: JSONException) { - throw RuntimeException(e1) + val confirm = + Runnable { + try { + getColUnsafe.modSchemaNoCheck() + repositionField(pos - 1) + } catch (e1: JSONException) { + throw RuntimeException(e1) + } } - } c.setConfirm(confirm) this@ModelFieldEditor.showDialogFragment(c) } @@ -377,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.moveFieldLegacy(notetype, noteFields.getJSONObject(currentPos), index) - true - } catch (e: ConfirmModSchemaException) { - e.log() - // Should never be reached - false + val result = + withCol { + Timber.d("doInBackgroundRepositionField") + try { + notetypes.moveFieldLegacy(notetype, noteFields.getJSONObject(currentPos), index) + true + } catch (e: ConfirmModSchemaException) { + e.log() + // Should never be reached + false + } } - } if (!result) { closeActivity() } @@ -401,8 +419,11 @@ 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 = noteFields.getJSONObject(currentPos) getColUnsafe.notetypes.renameFieldLegacy(notetype, field, fieldLabel) initialize() @@ -420,16 +441,20 @@ class ModelFieldEditor : AnkiActivity(), LocaleSelectionDialogHandler { // Handler mMod schema confirmation val c = ConfirmationDialog() c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = Runnable { - getColUnsafe.modSchemaNoCheck() - launchCatchingTask { changeSortField(notetype, currentPos) } - } + val confirm = + Runnable { + getColUnsafe.modSchemaNoCheck() + launchCatchingTask { changeSortField(notetype, 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)) { withCol { Timber.d("doInBackgroundChangeSortField") @@ -450,13 +475,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 ee982de9d55f..44803c9ea196 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/MyAccount.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/MyAccount.kt @@ -63,37 +63,44 @@ open class MyAccount : AnkiActivity() { private lateinit var loggedInLogo: ImageView // if the 'remove account' fragment is open, close it first - private val onRemoveAccountBackCallback = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - closeRemoveAccountScreen() + private val onRemoveAccountBackCallback = + object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + closeRemoveAccountScreen() + } } - } open fun switchToState(newState: Int) { when (newState) { STATE_LOGGED_IN -> { val username = baseContext.sharedPrefs().getString("username", "") usernameLoggedIn.text = username - toolbar = loggedIntoMyAccountView.findViewById(R.id.toolbar)?.also { toolbar -> - toolbar.title = - getString(R.string.sync_account) // This can be cleaned up if all three main layouts are guaranteed to share the same toolbar object - setSupportActionBar(toolbar) - } + toolbar = + loggedIntoMyAccountView.findViewById(R.id.toolbar)?.also { toolbar -> + // This can be cleaned up if all three main layouts are guaranteed + // to share the same toolbar object + toolbar.title = getString(R.string.sync_account) + setSupportActionBar(toolbar) + } setContentView(loggedIntoMyAccountView) } STATE_LOG_IN -> { - toolbar = loginToMyAccountView.findViewById(R.id.toolbar)?.also { toolbar -> - toolbar.title = getString(R.string.sync_account) // This can be cleaned up if all three main layouts are guaranteed to share the same toolbar object - setSupportActionBar(toolbar) - } + toolbar = + loginToMyAccountView.findViewById(R.id.toolbar)?.also { toolbar -> + // This can be cleaned up if all three main layouts + // are guaranteed to share the same toolbar object + toolbar.title = getString(R.string.sync_account) + setSupportActionBar(toolbar) + } setContentView(loginToMyAccountView) } } } - private val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { - Timber.i("notification permission: %b", it) - } + private val notificationPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { + Timber.i("notification permission: %b", it) + } override fun onCreate(savedInstanceState: Bundle?) { if (showedActivityFailedScreen(savedInstanceState)) { @@ -223,25 +230,36 @@ open class MyAccount : AnkiActivity() { } } false - } + }, ) - val textWatcher = object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - // Not needed here - } + val textWatcher = + object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int, + ) { + // Not needed here + } - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - val email = username.text.toString().trim() - val password = password.text.toString() - val isFilled = email.isNotEmpty() && password.isNotEmpty() - loginButton.isEnabled = isFilled - } + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int, + ) { + val email = username.text.toString().trim() + val password = password.text.toString() + val isFilled = email.isNotEmpty() && password.isNotEmpty() + loginButton.isEnabled = isFilled + } - override fun afterTextChanged(s: Editable?) { - // Not needed here + override fun afterTextChanged(s: Editable?) { + // Not needed here + } } - } username.addTextChangedListener(textWatcher) password.addTextChangedListener(textWatcher) loginButton.setOnClickListener { login() } @@ -255,19 +273,20 @@ open class MyAccount : AnkiActivity() { val lostEmail = loginToMyAccountView.findViewById