@@ -24,9 +24,11 @@ import android.database.Cursor
2424import android.database.CursorWindow
2525import android.net.Uri
2626import anki.cards.FsrsMemoryState
27+ import anki.collection.OpChanges
2728import anki.notetypes.StockNotetype
2829import com.ichi2.anki.CollectionManager
2930import com.ichi2.anki.FlashCardsContract
31+ import com.ichi2.anki.common.time.TimeManager
3032import com.ichi2.anki.common.utils.annotation.KotlinCleanup
3133import com.ichi2.anki.common.utils.emptyStringArray
3234import com.ichi2.anki.libanki.Card
@@ -44,6 +46,7 @@ import com.ichi2.anki.libanki.backend.BackendUtils
4446import com.ichi2.anki.libanki.exception.ConfirmModSchemaException
4547import com.ichi2.anki.libanki.getStockNotetype
4648import com.ichi2.anki.libanki.sched.Scheduler
49+ import com.ichi2.anki.observability.ChangeManager
4750import com.ichi2.anki.provider.pureAnswer
4851import com.ichi2.anki.testutil.DatabaseUtils.cursorFillWindow
4952import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission
@@ -70,6 +73,7 @@ import org.junit.Before
7073import org.junit.Rule
7174import org.junit.Test
7275import timber.log.Timber
76+ import kotlin.test.assertFailsWith
7377import kotlin.test.assertNotNull
7478import kotlin.test.assertTrue
7579import kotlin.test.junit.JUnitAsserter.assertNotNull
@@ -2462,6 +2466,166 @@ class ContentProviderTest : InstrumentedTest() {
24622466 return col
24632467 }
24642468
2469+ @Test
2470+ fun testInsertNotifiesUI () {
2471+ // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures
2472+ val counter = TestSubscriber ()
2473+ ChangeManager .subscribe(counter)
2474+ try {
2475+ val mid = noteTypeId
2476+ val noteType = col.notetypes.get(mid)!!
2477+ val fieldCount = noteType.fields.length()
2478+ val emptyFields = Array (fieldCount) { " " }.joinToString(separator = " \u001f " )
2479+
2480+ val values =
2481+ ContentValues ().apply {
2482+ put(FlashCardsContract .Note .MID , mid)
2483+ put(FlashCardsContract .Note .FLDS , emptyFields)
2484+ }
2485+
2486+ contentResolver.insert(FlashCardsContract .Note .CONTENT_URI , values)
2487+ assertNotificationReceived(counter)
2488+ } finally {
2489+ ChangeManager .unsubscribe(counter)
2490+ }
2491+ }
2492+
2493+ @Test
2494+ fun testUpdateNotifiesUI () {
2495+ // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures
2496+ val noteId = createdNotes.first().lastPathSegment!! .toLong()
2497+ val uri = Uri .withAppendedPath(FlashCardsContract .Note .CONTENT_URI , noteId.toString())
2498+ val values =
2499+ ContentValues ().apply {
2500+ put(FlashCardsContract .Note .TAGS , " new_tag" )
2501+ }
2502+ val counter = TestSubscriber ()
2503+ ChangeManager .subscribe(counter)
2504+ try {
2505+ contentResolver.update(uri, values, null , null )
2506+ assertNotificationReceived(counter)
2507+ } finally {
2508+ ChangeManager .unsubscribe(counter)
2509+ }
2510+ }
2511+
2512+ @Test
2513+ fun testUpdateNonExistentNoteDoesNotNotifyUI () {
2514+ // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures
2515+ val counter = TestSubscriber ()
2516+ ChangeManager .subscribe(counter)
2517+ try {
2518+ val values =
2519+ ContentValues ().apply {
2520+ put(FlashCardsContract .Note .TAGS , " new_tag" )
2521+ }
2522+ val uri = Uri .withAppendedPath(FlashCardsContract .Note .CONTENT_URI , " 999999" )
2523+
2524+ assertFailsWith<Exception > {
2525+ contentResolver.update(uri, values, null , null )
2526+ }
2527+
2528+ Thread .sleep(1000 )
2529+ assertEquals(" UI should not be notified if update is failed" , 0 , counter.count)
2530+ } finally {
2531+ ChangeManager .unsubscribe(counter)
2532+ }
2533+ }
2534+
2535+ @Test
2536+ fun testDeleteNotifiesUI () {
2537+ // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures
2538+ val noteId = createdNotes.first().lastPathSegment!! .toLong()
2539+ val uri = Uri .withAppendedPath(FlashCardsContract .Note .CONTENT_URI , noteId.toString())
2540+ val counter = TestSubscriber ()
2541+ ChangeManager .subscribe(counter)
2542+ try {
2543+ contentResolver.delete(uri, null , null )
2544+ assertNotificationReceived(counter)
2545+ } finally {
2546+ ChangeManager .unsubscribe(counter)
2547+ }
2548+ }
2549+
2550+ @Test
2551+ fun testDeleteNonExistentNoteDoesNotNotifyUI () {
2552+ // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures
2553+ val counter = TestSubscriber ()
2554+ ChangeManager .subscribe(counter)
2555+ try {
2556+ val uri = Uri .withAppendedPath(FlashCardsContract .Note .CONTENT_URI , " 999999" )
2557+ val deletedCount = contentResolver.delete(uri, null , null )
2558+ assertEquals(" It should return 0 for non-existent note" , 0 , deletedCount)
2559+
2560+ Thread .sleep(1000 )
2561+ assertEquals(" UI should not be notify if nothing was deleted" , 0 , counter.count)
2562+ } finally {
2563+ ChangeManager .unsubscribe(counter)
2564+ }
2565+ }
2566+
2567+ @Test
2568+ fun testBulkInsertNotifiesUI () {
2569+ // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures
2570+ val counter = TestSubscriber ()
2571+ ChangeManager .subscribe(counter)
2572+ try {
2573+ val mid = noteTypeId
2574+ val noteType = col.notetypes.get(mid)!!
2575+ val fieldCount = noteType.fields.length()
2576+ val emptyFields = Array (fieldCount) { " " }.joinToString(separator = " \u001f " )
2577+
2578+ val values =
2579+ arrayOf(
2580+ ContentValues ().apply {
2581+ put(FlashCardsContract .Note .MID , mid)
2582+ put(FlashCardsContract .Note .FLDS , emptyFields)
2583+ },
2584+ )
2585+
2586+ contentResolver.bulkInsert(FlashCardsContract .Note .CONTENT_URI , values)
2587+ assertNotificationReceived(counter)
2588+ } finally {
2589+ ChangeManager .unsubscribe(counter)
2590+ }
2591+ }
2592+
2593+ @Test
2594+ fun testBulkInsertEmptyListDoesNotNotifyUI () {
2595+ // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures -
2596+ val counter = TestSubscriber ()
2597+ ChangeManager .subscribe(counter)
2598+ try {
2599+ contentResolver.bulkInsert(FlashCardsContract .Note .CONTENT_URI , emptyArray())
2600+
2601+ Thread .sleep(1000 )
2602+ assertEquals(" UI should not be notified for empty bulk insert" , 0 , counter.count)
2603+ } finally {
2604+ ChangeManager .unsubscribe(counter)
2605+ }
2606+ }
2607+
2608+ private class TestSubscriber : ChangeManager .Subscriber {
2609+ var count = 0
2610+
2611+ override fun opExecuted (
2612+ changes : OpChanges ,
2613+ handler : Any? ,
2614+ ) {
2615+ count++
2616+ }
2617+ }
2618+
2619+ private fun assertNotificationReceived (subscriber : TestSubscriber ) {
2620+ val timeout = 5000L
2621+ val startTime = TimeManager .time.intTimeMS()
2622+ while (subscriber.count == 0 && TimeManager .time.intTimeMS() - startTime < timeout) {
2623+ Thread .sleep(100 )
2624+ }
2625+
2626+ assertTrue(" UI should be notified of the change" , subscriber.count > 0 )
2627+ }
2628+
24652629 private val contentResolver: ContentResolver
24662630 get() = testContext.contentResolver
24672631
0 commit comments