From 6a5e4c4b08443b06d86eb1bfbf11ebd23222fcc2 Mon Sep 17 00:00:00 2001 From: Aldana Belen Rodriguez Date: Mon, 28 Sep 2026 10:10:38 +0200 Subject: [PATCH 1/5] Add notification delivery health checks --- .../ankiquest/AnkiquestNotificationHealth.kt | 25 ++++++ .../ichi2/anki/ankiquest/AnkiquestNotifier.kt | 26 ++++++- .../com/ichi2/anki/ankiquest/AnkiquestPoll.kt | 1 + .../preferences/AnkiquestSettingsFragment.kt | 78 +++++++++++++++++-- .../ankiquest_notification_health.xml | 19 +++++ .../values/ankiquest_notification_health.xml | 20 +++++ .../main/res/xml/preferences_ankiquest.xml | 5 ++ .../ichi2/anki/ankiquest/AnkiquestPollTest.kt | 24 ++++++ 8 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealth.kt create mode 100644 AnkiDroid/src/main/res/values-es/ankiquest_notification_health.xml create mode 100644 AnkiDroid/src/main/res/values/ankiquest_notification_health.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealth.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealth.kt new file mode 100644 index 000000000000..186a2ce521c7 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealth.kt @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package com.ichi2.anki.ankiquest + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.ichi2.anki.AnkiDroidApp + +/** The inbox timestamp belongs to the authenticated server account that fetched it. */ +object AnkiquestNotificationHealth { + private const val SYNC_KEY = "ankiquestInboxSyncedAt:" + + fun recordSuccessfulSync( + scope: String, + at: Long, + preferences: SharedPreferences = AnkiDroidApp.sharedPrefs(), + ) { + if (AnkiquestHomeData.account(preferences)?.scope != scope) return + preferences.edit { putLong("$SYNC_KEY$scope", at) } + } + + fun lastSuccessfulSync(preferences: SharedPreferences = AnkiDroidApp.sharedPrefs()): Long? { + val account = AnkiquestHomeData.account(preferences)?.takeIf { it.token.isNotEmpty() } ?: return null + return preferences.getLong("$SYNC_KEY${account.scope}", 0L).takeIf { it > 0L } + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt index 2cbef00cc236..bf02c342fdb9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt @@ -50,6 +50,7 @@ object AnkiquestNotifier { private const val STREAK_DAY_KEY = "ankiquestStreakNotifiedDay" private const val RANK_ID = 5_130_001 private const val STREAK_ID = 5_130_002 + private const val TEST_ID = 5_130_003 private const val HOUR_MS = 60 * 60 * 1000L @Synchronized @@ -237,6 +238,29 @@ object AnkiquestNotifier { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null)) } + /** A local alert checks Android delivery without sending a server message. */ + fun testAlert(context: Context): Delivery = + notify( + context, + TEST_ID, + AnkiquestLanguage.context(context).getString(R.string.ankiquest_health_test_title), + AnkiquestLanguage.context(context).getString(R.string.ankiquest_health_test_body), + context.packageManager.getLaunchIntentForPackage(context.packageName) + ?: Intent(context, AnkiquestHomeActivity::class.java), + ) + + fun alertsEnabled( + context: Context, + nudge: Boolean, + ): Boolean { + val manager = NotificationManagerCompat.from(context) + if (!manager.areNotificationsEnabled()) return false + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true + return manager.getNotificationChannel(ensureChannel(context, nudge))?.importance?.let { + it != NotificationManagerCompat.IMPORTANCE_NONE + } ?: false + } + private fun ensureChannel( context: Context, nudge: Boolean, @@ -260,7 +284,7 @@ object AnkiquestNotifier { return channel } - private enum class Delivery { + enum class Delivery { POSTED, CHANNEL_BLOCKED, DISABLED, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestPoll.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestPoll.kt index 50d87dfd3f8e..8d9699dc470e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestPoll.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestPoll.kt @@ -124,6 +124,7 @@ object AnkiquestPoll { val result = try { Ankiquest.completionNotifications()?.let { (account, notifications, scope) -> + AnkiquestNotificationHealth.recordSuccessfulSync(scope, TimeManager.time.intTimeMS(), prefs) AnkiquestNotifier.onDeckCompletions(context, account, notifications, scope) } Result.success() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt index 570b5618d381..05f35ced8e3a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt @@ -19,6 +19,7 @@ import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Build +import android.text.format.DateFormat import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat @@ -36,6 +37,7 @@ import com.ichi2.anki.ankiquest.AnkiquestAvatarEditor import com.ichi2.anki.ankiquest.AnkiquestAvatars import com.ichi2.anki.ankiquest.AnkiquestDeckAdapter import com.ichi2.anki.ankiquest.AnkiquestDeckTree +import com.ichi2.anki.ankiquest.AnkiquestNotificationHealth import com.ichi2.anki.ankiquest.AnkiquestNotifier import com.ichi2.anki.ankiquest.AnkiquestPoll import com.ichi2.anki.ankiquest.AnkiquestUpdater @@ -45,6 +47,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import okhttp3.HttpUrl.Companion.toHttpUrl import org.json.JSONObject +import java.util.Date class AnkiquestSettingsFragment : SettingsFragment() { override val preferenceResource = R.xml.preferences_ankiquest @@ -146,6 +149,10 @@ class AnkiquestSettingsFragment : SettingsFragment() { } bindAlertSettings(R.string.ankiquest_message_alerts_key, nudge = false) bindAlertSettings(R.string.ankiquest_nudge_alerts_key, nudge = true) + requirePreference(R.string.ankiquest_health_key).setOnPreferenceClickListener { + showNotificationHealth() + true + } bindAction(R.string.ankiquest_test_key) { Ankiquest.runFromSettings(requireContext(), uploadAll = false) } bindAction(R.string.ankiquest_upload_all_key) { Ankiquest.runFromSettings(requireContext(), uploadAll = true) } bindAction(R.string.ankiquest_deck_notifications_key) { @@ -197,15 +204,76 @@ class AnkiquestSettingsFragment : SettingsFragment() { nudge: Boolean, ) { requirePreference(key).setOnPreferenceClickListener { - try { - startActivity(AnkiquestNotifier.alertSettingsIntent(requireContext(), nudge)) - } catch (_: ActivityNotFoundException) { - openAppSettingsScreen() - } + openAlertSettings(nudge) true } } + private fun openAlertSettings(nudge: Boolean) { + try { + startActivity(AnkiquestNotifier.alertSettingsIntent(requireContext(), nudge)) + } catch (_: ActivityNotFoundException) { + openAppSettingsScreen() + } + } + + private fun showNotificationHealth() { + val context = requireContext() + val permissionGranted = + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + val state = { enabled: Boolean -> getString(if (enabled) R.string.ankiquest_health_allowed else R.string.ankiquest_health_blocked) } + val lastSync = + AnkiquestNotificationHealth.lastSuccessfulSync()?.let { at -> + val date = Date(at) + "${DateFormat.getDateFormat(context).format(date)} ${DateFormat.getTimeFormat(context).format(date)}" + } ?: getString(R.string.ankiquest_health_never) + val details = + listOf( + getString(R.string.ankiquest_health_permission, state(permissionGranted)), + getString(R.string.ankiquest_health_messages, state(AnkiquestNotifier.alertsEnabled(context, false))), + getString(R.string.ankiquest_health_nudges, state(AnkiquestNotifier.alertsEnabled(context, true))), + getString(R.string.ankiquest_health_last_sync, lastSync), + ).joinToString("\n") + AlertDialog + .Builder(context) + .setTitle(R.string.ankiquest_health_title) + .setMessage(details) + .setItems( + arrayOf( + getString(R.string.ankiquest_health_test), + getString(R.string.ankiquest_health_permission_settings), + getString(R.string.ankiquest_message_alerts_title), + getString(R.string.ankiquest_nudge_alerts_title), + ), + ) { _, choice -> + when (choice) { + 0 -> { + val message = + when (AnkiquestNotifier.testAlert(context)) { + AnkiquestNotifier.Delivery.POSTED -> R.string.ankiquest_health_test_posted + AnkiquestNotifier.Delivery.CHANNEL_BLOCKED -> R.string.ankiquest_health_test_blocked + AnkiquestNotifier.Delivery.DISABLED -> R.string.ankiquest_health_test_disabled + } + AlertDialog + .Builder(context) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show() + } + 1 -> + if (!permissionGranted) { + notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + openAppSettingsScreen() + } + 2 -> openAlertSettings(nudge = false) + 3 -> openAlertSettings(nudge = true) + } + }.setNegativeButton(android.R.string.cancel, null) + .show() + } + private fun bindDashboard( key: Int, community: Boolean = false, diff --git a/AnkiDroid/src/main/res/values-es/ankiquest_notification_health.xml b/AnkiDroid/src/main/res/values-es/ankiquest_notification_health.xml new file mode 100644 index 000000000000..da3d45ef6712 --- /dev/null +++ b/AnkiDroid/src/main/res/values-es/ankiquest_notification_health.xml @@ -0,0 +1,19 @@ + + + Estado de las notificaciones + Comprueba la entrega y envía un aviso de prueba + Permiso de notificaciones: %1$s + Avisos de mensajes: %1$s + Avisos de toques: %1$s + Última sincronización de mensajes: %1$s + Permitido + Bloqueado en los ajustes de Android + Nunca para esta cuenta + Enviar aviso de prueba + Abrir permisos de notificaciones + Aviso de prueba de AnkiQuest + Si ves esto, los avisos locales funcionan. + Aviso de prueba enviado. El teléfono controla los banners, el sonido y la vibración. + Los avisos de mensajes están desactivados. Actívalos en los ajustes de Android. + AnkiQuest no tiene permiso para mostrar notificaciones. Actívalo en los ajustes de Android. + diff --git a/AnkiDroid/src/main/res/values/ankiquest_notification_health.xml b/AnkiDroid/src/main/res/values/ankiquest_notification_health.xml new file mode 100644 index 000000000000..77e3b9cf6700 --- /dev/null +++ b/AnkiDroid/src/main/res/values/ankiquest_notification_health.xml @@ -0,0 +1,20 @@ + + + ankiquestNotificationHealth + Notification health + Check delivery and send a test alert + Notification permission: %1$s + Message alerts: %1$s + Nudge alerts: %1$s + Last inbox sync: %1$s + Allowed + Blocked in Android settings + Never for this account + Send test alert + Open notification permissions + AnkiQuest test alert + If you see this, local alerts are working. + Test alert sent. Your phone controls banners, sound and vibration. + The message alert channel is off. Open its Android settings to enable it. + Notifications are not allowed for AnkiQuest. Enable them in Android settings. + diff --git a/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml b/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml index be4f0f1bdfbb..ca39b527330f 100644 --- a/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml +++ b/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml @@ -73,6 +73,11 @@ android:persistent="false" android:title="@string/ankiquest_nudge_alerts_title" android:summary="@string/ankiquest_alerts_summary" /> + 0L) + + AnkiDroidApp.sharedPrefs().edit { putString(Ankiquest.USER_KEY, "other") } + assertNull(AnkiquestNotificationHealth.lastSuccessfulSync()) + AnkiDroidApp.sharedPrefs().edit { putString(Ankiquest.USER_KEY, "cerro") } + assertEquals(synced, AnkiquestNotificationHealth.lastSuccessfulSync()) + } + + @Test + fun `failed inbox sync does not claim success`() = + runBlocking { + inboxStatus = 503 + assertEquals(Result.retry(), worker().doWork()) + assertNull(AnkiquestNotificationHealth.lastSuccessfulSync()) + } + @Test fun `corrupt cached leaderboard does not prevent inbox delivery`() = runBlocking { From be14ece8a06d24e6c1d5cde9e9f2611922b1ca0d Mon Sep 17 00:00:00 2001 From: Aldana Belen Rodriguez Date: Mon, 28 Sep 2026 10:57:28 +0200 Subject: [PATCH 2/5] Register notification health preference for CI checks --- AnkiDroid/src/main/res/values/ankiquest_notification_health.xml | 2 +- .../java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/res/values/ankiquest_notification_health.xml b/AnkiDroid/src/main/res/values/ankiquest_notification_health.xml index 77e3b9cf6700..736178379885 100644 --- a/AnkiDroid/src/main/res/values/ankiquest_notification_health.xml +++ b/AnkiDroid/src/main/res/values/ankiquest_notification_health.xml @@ -1,7 +1,7 @@ ankiquestNotificationHealth - Notification health + Notification health Check delivery and send a test alert Notification permission: %1$s Message alerts: %1$s diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt index d25d5de5b390..3c1f642bbdef 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/analytics/PreferencesAnalyticsTest.kt @@ -111,6 +111,7 @@ class PreferencesAnalyticsTest : RobolectricTest() { R.string.ankiquest_nudges_key, R.string.ankiquest_message_alerts_key, R.string.ankiquest_nudge_alerts_key, + R.string.ankiquest_health_key, R.string.ankiquest_dashboard_key, R.string.ankiquest_community_reminders_key, R.string.ankiquest_streak_protection_key, From 55e97b030cab546e6cc848561fc7a8bc1245dd5e Mon Sep 17 00:00:00 2001 From: Aldana Belen Rodriguez Date: Mon, 28 Sep 2026 11:54:00 +0200 Subject: [PATCH 3/5] Make private access assertions robust to extra background requests --- .../ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt index ce77fa8a912a..449bd453c59e 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt @@ -146,8 +146,9 @@ class AnkiquestPrivateAccessTest : RobolectricTest() { Ankiquest.profile() - assertEquals("/api/profile/member%20name", requests.single().path) - assertEquals("Bearer member-secret", requests.single().authorization) + assertTrue(requests.any { it.path == "/api/profile/member%20name" }) + assertTrue(requests.filter { it.path == "/api/profile/member%20name" }.all { it.authorization == "Bearer member-secret" }) + assertTrue(requests.none { it.authorization == "Bearer new-private-secret" }) } @Test @@ -158,8 +159,8 @@ class AnkiquestPrivateAccessTest : RobolectricTest() { Ankiquest.runFromSettings(targetContext, uploadAll = false) - assertEquals("/api/profile/member%20name", requests.single().path) - assertNull(requests.single().authorization) + assertTrue(requests.any { it.path == "/api/profile/member%20name" }) + assertTrue(requests.all { it.authorization == null }) } @Test From 1908004d2644d158739626db4d0e23c57f264923 Mon Sep 17 00:00:00 2001 From: Aldana Belen Rodriguez Date: Mon, 28 Sep 2026 12:24:02 +0200 Subject: [PATCH 4/5] Keep notification health actions visible with status details --- .../preferences/AnkiquestSettingsFragment.kt | 62 ++++++++++--------- .../AnkiquestNotificationHealthDialogTest.kt | 53 ++++++++++++++++ 2 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealthDialogTest.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt index 05f35ced8e3a..ccb241117f80 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AnkiquestSettingsFragment.kt @@ -239,37 +239,41 @@ class AnkiquestSettingsFragment : SettingsFragment() { .Builder(context) .setTitle(R.string.ankiquest_health_title) .setMessage(details) - .setItems( - arrayOf( - getString(R.string.ankiquest_health_test), - getString(R.string.ankiquest_health_permission_settings), - getString(R.string.ankiquest_message_alerts_title), - getString(R.string.ankiquest_nudge_alerts_title), - ), - ) { _, choice -> - when (choice) { - 0 -> { - val message = - when (AnkiquestNotifier.testAlert(context)) { - AnkiquestNotifier.Delivery.POSTED -> R.string.ankiquest_health_test_posted - AnkiquestNotifier.Delivery.CHANNEL_BLOCKED -> R.string.ankiquest_health_test_blocked - AnkiquestNotifier.Delivery.DISABLED -> R.string.ankiquest_health_test_disabled - } - AlertDialog - .Builder(context) - .setMessage(message) - .setPositiveButton(android.R.string.ok, null) - .show() + .setPositiveButton(R.string.ankiquest_health_test) { _, _ -> + val message = + when (AnkiquestNotifier.testAlert(context)) { + AnkiquestNotifier.Delivery.POSTED -> R.string.ankiquest_health_test_posted + AnkiquestNotifier.Delivery.CHANNEL_BLOCKED -> R.string.ankiquest_health_test_blocked + AnkiquestNotifier.Delivery.DISABLED -> R.string.ankiquest_health_test_disabled } - 1 -> - if (!permissionGranted) { - notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) - } else { - openAppSettingsScreen() + AlertDialog + .Builder(context) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show() + }.setNeutralButton(R.string.settings) { _, _ -> + AlertDialog + .Builder(context) + .setTitle(R.string.settings) + .setItems( + arrayOf( + getString(R.string.ankiquest_health_permission_settings), + getString(R.string.ankiquest_message_alerts_title), + getString(R.string.ankiquest_nudge_alerts_title), + ), + ) { _, choice -> + when (choice) { + 0 -> + if (!permissionGranted) { + notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + openAppSettingsScreen() + } + 1 -> openAlertSettings(nudge = false) + 2 -> openAlertSettings(nudge = true) } - 2 -> openAlertSettings(nudge = false) - 3 -> openAlertSettings(nudge = true) - } + }.setNegativeButton(android.R.string.cancel, null) + .show() }.setNegativeButton(android.R.string.cancel, null) .show() } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealthDialogTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealthDialogTest.kt new file mode 100644 index 000000000000..575dd71d355f --- /dev/null +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestNotificationHealthDialogTest.kt @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package com.ichi2.anki.ankiquest + +import android.os.Looper +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.preference.Preference +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ichi2.anki.R +import com.ichi2.anki.RobolectricTest +import com.ichi2.anki.preferences.AnkiquestSettingsFragment +import com.ichi2.anki.preferences.PreferencesActivity +import com.ichi2.anki.preferences.PreferencesFragment +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowDialog +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class AnkiquestNotificationHealthDialogTest : RobolectricTest() { + @Test + fun `notification status and actions are both visible`() { + val intent = PreferencesActivity.getIntent(targetContext, AnkiquestSettingsFragment::class) + val controller = Robolectric.buildActivity(PreferencesActivity::class.java, intent).setup() + saveControllerForCleanup(controller) + val fragment = + (controller.get().fragment as PreferencesFragment) + .childFragmentManager + .findFragmentById(R.id.settings_container) as AnkiquestSettingsFragment + val health = assertNotNull(fragment.findPreference(targetContext.getString(R.string.ankiquest_health_key))) + + health.performClick() + + val dialog = assertNotNull(ShadowDialog.getLatestDialog() as? AlertDialog) + assertTrue( + assertNotNull( + dialog.findViewById(android.R.id.message), + ).text.contains(targetContext.getString(R.string.ankiquest_health_last_sync, "")), + ) + assertTrue(assertNotNull(dialog.getButton(AlertDialog.BUTTON_POSITIVE)).isShown) + val settingsButton = assertNotNull(dialog.getButton(AlertDialog.BUTTON_NEUTRAL)) + assertTrue(settingsButton.isShown) + settingsButton.performClick() + shadowOf(Looper.getMainLooper()).idle() + val settings = assertNotNull(ShadowDialog.getLatestDialog() as? AlertDialog) + assertEquals(3, assertNotNull(settings.listView).adapter.count) + } +} From 9fc9f2c3dc44f06fba10230b0b47817a41f1eef0 Mon Sep 17 00:00:00 2001 From: Aldana Belen Rodriguez Date: Mon, 28 Sep 2026 13:00:50 +0200 Subject: [PATCH 5/5] Stabilize account-switch test under background reads --- .../ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt index 449bd453c59e..25bc16f83cd0 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ankiquest/AnkiquestPrivateAccessTest.kt @@ -118,6 +118,8 @@ class AnkiquestPrivateAccessTest : RobolectricTest() { private fun switchServerDuringSettingsRead() { val preferences = targetContext.sharedPrefs() val changingPreferences = mockk() + val originalSettings = preferences.all + val originalUrl = preferences.getString(Ankiquest.URL_KEY, "") fun switchServer() { preferences.edit { @@ -127,14 +129,12 @@ class AnkiquestPrivateAccessTest : RobolectricTest() { } every { changingPreferences.getString(any(), any()) } answers { preferences.getString(firstArg(), secondArg()) } every { changingPreferences.getString(Ankiquest.URL_KEY, "") } answers { - val captured = preferences.getString(Ankiquest.URL_KEY, "") switchServer() - captured + originalUrl } every { changingPreferences.all } answers { - val captured = preferences.all switchServer() - captured + originalSettings } AnkiDroidApp.sharedPreferencesTestingOverride = changingPreferences }