From 642a03254e3b0a810ff38007cefb4f5e96e98480 Mon Sep 17 00:00:00 2001 From: snazarenko Date: Tue, 30 Jun 2026 21:31:58 +0300 Subject: [PATCH 1/4] feat: add card flags to CardContentProvider and api --- .../ichi2/anki/tests/ContentProviderTest.kt | 48 ++++++++++++++++ .../anki/provider/CardContentProvider.kt | 45 +++++++++------ .../java/com/ichi2/anki/FlashCardsContract.kt | 17 ++++++ .../java/com/ichi2/anki/api/AddContentApi.kt | 55 +++++++++++++++++++ api/src/main/java/com/ichi2/anki/api/Flag.kt | 21 +++++++ 5 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 api/src/main/java/com/ichi2/anki/api/Flag.kt 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 124cb6f12767..88e166a458d3 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -2598,6 +2598,54 @@ class ContentProviderTest : InstrumentedTest() { } } + /** + * Check that set the flag in card works as expected + */ + @Test + fun testSetFlagCard() { + val noteId = createdNotes.first().lastPathSegment!!.toLong() + val noteUri = Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, noteId.toString()) + val noteCardsUri = Uri.withAppendedPath(noteUri, "cards") + val card = col.getNote(noteId).cards(col).single() + val noteCardUri = Uri.withAppendedPath(noteCardsUri, card.ord.toString()) + + val negativeValue = + ContentValues().apply { + put(FlashCardsContract.Card.FLAGS, -1) + } + val exception1 = assertThrows { contentResolver.update(noteCardUri, negativeValue, null, null) } + assertEquals(exception1.message, "Flags value must be in the range from 0 to 7") + + val tooLargeValue = + ContentValues().apply { + put(FlashCardsContract.Card.FLAGS, 8) + } + val exception2 = assertThrows { contentResolver.update(noteCardUri, tooLargeValue, null, null) } + assertEquals(exception2.message, "Flags value must be in the range from 0 to 7") + + val correctValue = + ContentValues().apply { + put(FlashCardsContract.Card.FLAGS, 4) + } + contentResolver.update(noteCardUri, correctValue, null, null) + + val cursor = + checkNotNull( + contentResolver.query( + noteCardUri, + arrayOf(FlashCardsContract.Card.FLAGS), + null, + null, + null, + ), + ) { "cursor from /notes/#/cards/#" } + + cursor.use { + assertTrue(it.moveToFirst()) + assertEquals(4, it.getInt(it.getColumnIndex(FlashCardsContract.Card.FLAGS))) + } + } + // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures private class TestSubscriber : ChangeManager.Subscriber { var count = 0 diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt index d08e23a82a92..a852b32258af 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt @@ -538,28 +538,38 @@ class CardContentProvider : ContentProvider() { NOTES_ID_CARDS -> throw UnsupportedOperationException("Not yet implemented") NOTES_ID_CARDS_ORD -> { val currentCard = getCardFromUri(uri, col) - var isDeckUpdate = false - var did = Decks.NOT_FOUND_DECK_ID // the key of the ContentValues contains the column name // the value of the ContentValues contains the row value. val valueSet = values!!.valueSet() for ((key) in valueSet) { - // Only updates on deck id is supported - isDeckUpdate = key == FlashCardsContract.Card.DECK_ID - did = values.getAsLong(key) - } - require(!col.decks.isFiltered(did)) { "Cards cannot be moved to a filtered deck" } - /* now update the card - */ - if (isDeckUpdate && did >= 0) { - Timber.d("CardContentProvider: Moving card to other deck...") - currentCard.did = did - col.updateCard(currentCard) + when (key) { + FlashCardsContract.Card.DECK_ID -> { + val did = values.getAsLong(key) + require(!col.decks.isFiltered(did)) { "Cards cannot be moved to a filtered deck" } - updated++ - } else { - // User tries an operation that is not (yet?) supported. - throw IllegalArgumentException("Currently only updates of decks are supported") + Timber.d("CardContentProvider: Moving card to other deck...") + currentCard.did = did + col.updateCard(currentCard) + + updated++ + } + FlashCardsContract.Card.FLAGS -> { + val flags = values.getAsInteger(key) + + if (flags == null || flags < 0 || flags > 7) { + throw IllegalArgumentException("Flags value must be in the range from 0 to 7") + } + + Timber.d("CardContentProvider: flags update...") + currentCard.flags = flags + col.updateCard(currentCard) + updated++ + } + else -> { + // User tries an operation that is not (yet?) supported. + throw IllegalArgumentException("Currently only updates of decks and flags are supported") + } + } } } NOTE_TYPES -> throw IllegalArgumentException("Cannot update models in bulk") @@ -1223,6 +1233,7 @@ class CardContentProvider : ContentProvider() { FlashCardsContract.Card.FSRS_DESIRED_RETENTION -> rb.add(currentCard.fsrsDesiredRetention) FlashCardsContract.Card.FSRS_DECAY -> rb.add(currentCard.decay) FlashCardsContract.Card.LAST_REVIEW_TIME_SECONDS -> rb.add(currentCard.lastReviewTimeSecs) + FlashCardsContract.Card.FLAGS -> rb.add(currentCard.flags) else -> throw UnsupportedOperationException("Queue \"$column\" is unknown") } } diff --git a/api/src/main/java/com/ichi2/anki/FlashCardsContract.kt b/api/src/main/java/com/ichi2/anki/FlashCardsContract.kt index 45d70fb26acc..19d6052c6ce7 100644 --- a/api/src/main/java/com/ichi2/anki/FlashCardsContract.kt +++ b/api/src/main/java/com/ichi2/anki/FlashCardsContract.kt @@ -890,6 +890,22 @@ public object FlashCardsContract { */ public const val LAST_REVIEW_TIME_SECONDS: String = "last_review_time_secs" + /** + * The raw flag code in the range from 0 to 7 + * + * * `0` = no flag + * * `1` = red + * * `2` = orange + * * `3` = green + * * `4` = blue + * * `5` = pink + * * `6` = turquoise + * * `7` = purple + * + * Other values will throw IllegalArgumentException on set. + */ + public const val FLAGS: String = "flags" + /** * The content:// style URI for cards. Can be used to search for cards or access specific cards. * For examples on how to use the URI for queries see the overview in [FlashCardsContract]. @@ -907,6 +923,7 @@ public object FlashCardsContract { DECK_ID, QUESTION, ANSWER, + FLAGS, ) /** diff --git a/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt b/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt index f5075e82c171..55028b37eb9a 100644 --- a/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt +++ b/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt @@ -501,6 +501,61 @@ public class AddContentApi( } } + /** + * Set the flag using flag code for a given note + * @param noteId the ID of the note to update + * @param rawFlag the flag code in the range from 0 to 7 to set + * @return true if flag was updated, otherwise false + * @throws SecurityException if READ_WRITE_PERMISSION not granted (e.g. due to install order bug) + * @throws IllegalArgumentException if flag code not in the range from 0 to 7 + */ + public fun setFlagRaw( + noteId: Long, + rawFlag: Int, + ): Boolean { + val cardsUri = + Note.CONTENT_URI + .buildUpon() + .appendPath(noteId.toString()) + .appendPath("cards") + .build() + + val cardsQuery = resolver.query(cardsUri, null, null, null, null) ?: return false + + val values = + ContentValues().apply { + put(Card.FLAGS, rawFlag) + } + var numRowsUpdated = 0 + + cardsQuery.use { cardsCursor -> + while (cardsCursor.moveToNext()) { + val cardUri = + Note.CONTENT_URI + .buildUpon() + .appendPath(noteId.toString()) + .appendPath("cards") + .appendPath(cardsCursor.getString(cardsCursor.getColumnIndex(Card.CARD_ORD))) + .build() + numRowsUpdated += resolver.update(cardUri, values, null, null) + } + } + + return numRowsUpdated > 0 + } + + /** + * Set the flag for a given note + * @param noteId the ID of the note to update + * @param flag the flag to set + * @return true if flag was updated, otherwise false + * @throws SecurityException if READ_WRITE_PERMISSION not granted (e.g. due to install order bug) + */ + public fun setFlag( + noteId: Long, + flag: Flag, + ): Boolean = setFlagRaw(noteId, flag.code) + /** * Get the name of the selected deck * @return deck name or null if there was a problem diff --git a/api/src/main/java/com/ichi2/anki/api/Flag.kt b/api/src/main/java/com/ichi2/anki/api/Flag.kt new file mode 100644 index 000000000000..81290339f714 --- /dev/null +++ b/api/src/main/java/com/ichi2/anki/api/Flag.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 ColgateTotal77 +// SPDX-License-Identifier: LGPL-3.0-or-later +package com.ichi2.anki.api + +/* + * [code] should be kept in sync with the [com.ichi2.anki.Flag] enum. + * + * param code The code of the flag. + */ +public enum class Flag( + public val code: Int, +) { + NONE(0), + RED(1), + ORANGE(2), + GREEN(3), + BLUE(4), + PINK(5), + TURQUOISE(6), + PURPLE(7), +} From 151fdc4b8d21cdaffc00c12407e870b9a631826a Mon Sep 17 00:00:00 2001 From: snazarenko Date: Wed, 1 Jul 2026 21:49:20 +0300 Subject: [PATCH 2/4] fix: replace explicit flag set with setUserFlag in CardContentProvider.kt and update testSetFlagCard --- .../java/com/ichi2/anki/tests/ContentProviderTest.kt | 5 ++++- .../main/java/com/ichi2/anki/provider/CardContentProvider.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 88e166a458d3..c81a8c9df352 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -2623,6 +2623,9 @@ class ContentProviderTest : InstrumentedTest() { val exception2 = assertThrows { contentResolver.update(noteCardUri, tooLargeValue, null, null) } assertEquals(exception2.message, "Flags value must be in the range from 0 to 7") + card.flags = 8 + col.updateCard(card) + val correctValue = ContentValues().apply { put(FlashCardsContract.Card.FLAGS, 4) @@ -2642,7 +2645,7 @@ class ContentProviderTest : InstrumentedTest() { cursor.use { assertTrue(it.moveToFirst()) - assertEquals(4, it.getInt(it.getColumnIndex(FlashCardsContract.Card.FLAGS))) + assertEquals(12, it.getInt(it.getColumnIndex(FlashCardsContract.Card.FLAGS))) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt index a852b32258af..007ccf2376f7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt @@ -561,7 +561,7 @@ class CardContentProvider : ContentProvider() { } Timber.d("CardContentProvider: flags update...") - currentCard.flags = flags + currentCard.setUserFlag(flags) col.updateCard(currentCard) updated++ } From bada803c16877b2efd43ce86483ef38b4587abde Mon Sep 17 00:00:00 2001 From: snazarenko Date: Tue, 14 Jul 2026 14:23:43 +0300 Subject: [PATCH 3/4] fix: replace hardcoded flag bounds with max/min values from backend Flag enum. Create separate test for SetUserFlag --- .../ichi2/anki/tests/ContentProviderTest.kt | 58 ++++++++++++++++--- .../src/main/java/com/ichi2/anki/Flag.kt | 3 + .../anki/provider/CardContentProvider.kt | 7 ++- .../java/com/ichi2/anki/api/AddContentApi.kt | 6 +- api/src/main/java/com/ichi2/anki/api/Flag.kt | 6 ++ 5 files changed, 65 insertions(+), 15 deletions(-) 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 c81a8c9df352..295eaa3f38f7 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -27,6 +27,7 @@ import anki.cards.FsrsMemoryState import anki.collection.OpChanges import anki.notetypes.StockNotetype import com.ichi2.anki.CollectionManager +import com.ichi2.anki.Flag import com.ichi2.anki.FlashCardsContract import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.common.utils.annotation.KotlinCleanup @@ -2611,24 +2612,21 @@ class ContentProviderTest : InstrumentedTest() { val negativeValue = ContentValues().apply { - put(FlashCardsContract.Card.FLAGS, -1) + put(FlashCardsContract.Card.FLAGS, Flag.MIN_CODE - 1) } val exception1 = assertThrows { contentResolver.update(noteCardUri, negativeValue, null, null) } - assertEquals(exception1.message, "Flags value must be in the range from 0 to 7") + assertEquals(exception1.message, "Flags value must be in the range from ${Flag.MIN_CODE} to ${Flag.MAX_CODE}") val tooLargeValue = ContentValues().apply { - put(FlashCardsContract.Card.FLAGS, 8) + put(FlashCardsContract.Card.FLAGS, Flag.MAX_CODE + 1) } val exception2 = assertThrows { contentResolver.update(noteCardUri, tooLargeValue, null, null) } - assertEquals(exception2.message, "Flags value must be in the range from 0 to 7") - - card.flags = 8 - col.updateCard(card) + assertEquals(exception2.message, "Flags value must be in the range from ${Flag.MIN_CODE} to ${Flag.MAX_CODE}") val correctValue = ContentValues().apply { - put(FlashCardsContract.Card.FLAGS, 4) + put(FlashCardsContract.Card.FLAGS, Flag.MAX_CODE) } contentResolver.update(noteCardUri, correctValue, null, null) @@ -2645,7 +2643,49 @@ class ContentProviderTest : InstrumentedTest() { cursor.use { assertTrue(it.moveToFirst()) - assertEquals(12, it.getInt(it.getColumnIndex(FlashCardsContract.Card.FLAGS))) + assertEquals(Flag.MAX_CODE, it.getInt(it.getColumnIndex(FlashCardsContract.Card.FLAGS))) + } + } + + /** + * Check that setUserFlag change only first 3 bits of flags + */ + @Test + fun testSetUserFlag() { + val noteId = createdNotes.first().lastPathSegment!!.toLong() + val noteUri = Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, noteId.toString()) + val noteCardsUri = Uri.withAppendedPath(noteUri, "cards") + val card = col.getNote(noteId).cards(col).single() + val noteCardUri = Uri.withAppendedPath(noteCardsUri, card.ord.toString()) + + // set 8 (1000 in binary) manually + card.flags = 8 + col.updateCard(card) + + // set 4 (100 in binary) using setUserFlag method + card.setUserFlag(4) + col.updateCard(card) + + val cursor = + checkNotNull( + contentResolver.query( + noteCardUri, + arrayOf(FlashCardsContract.Card.FLAGS), + null, + null, + null, + ), + ) { "cursor from /notes/#/cards/#" } + + // If we override only first 3 bits, we won't affect fourth. + // Result will be 12 (1000 | 0100 = 1100 in binary). + val updatedCard = col.getNote(noteId).cards(col).single() + assertEquals(12, updatedCard.flags) + + // But cursor should return only first 3 bits + cursor.use { + assertTrue(it.moveToFirst()) + assertEquals(4, it.getInt(it.getColumnIndex(FlashCardsContract.Card.FLAGS))) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt index ada5344fd891..ceb039d5eb12 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt @@ -144,6 +144,9 @@ enum class Flag( fun toSearchNode(): SearchNode = searchNode { flag = toSearchValue() } companion object { + val MAX_CODE: Int = entries.maxOf { it.code } + val MIN_CODE: Int = entries.minOf { it.code } + fun fromCode(code: Int) = Flag.entries.first { it.code == code } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt index 007ccf2376f7..31e056179338 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt @@ -35,6 +35,7 @@ import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.BuildConfig import com.ichi2.anki.CollectionManager import com.ichi2.anki.FlashCardsContract +import com.ichi2.anki.api.Flag import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.common.utils.annotation.KotlinCleanup @@ -556,8 +557,8 @@ class CardContentProvider : ContentProvider() { FlashCardsContract.Card.FLAGS -> { val flags = values.getAsInteger(key) - if (flags == null || flags < 0 || flags > 7) { - throw IllegalArgumentException("Flags value must be in the range from 0 to 7") + if (flags == null || flags < Flag.MIN_CODE || flags > Flag.MAX_CODE) { + throw IllegalArgumentException("Flags value must be in the range from ${Flag.MIN_CODE} to ${Flag.MAX_CODE}") } Timber.d("CardContentProvider: flags update...") @@ -1233,7 +1234,7 @@ class CardContentProvider : ContentProvider() { FlashCardsContract.Card.FSRS_DESIRED_RETENTION -> rb.add(currentCard.fsrsDesiredRetention) FlashCardsContract.Card.FSRS_DECAY -> rb.add(currentCard.decay) FlashCardsContract.Card.LAST_REVIEW_TIME_SECONDS -> rb.add(currentCard.lastReviewTimeSecs) - FlashCardsContract.Card.FLAGS -> rb.add(currentCard.flags) + FlashCardsContract.Card.FLAGS -> rb.add(currentCard.flags and 7) // return only 3 first bits else -> throw UnsupportedOperationException("Queue \"$column\" is unknown") } } diff --git a/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt b/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt index 55028b37eb9a..55236a51b340 100644 --- a/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt +++ b/api/src/main/java/com/ichi2/anki/api/AddContentApi.kt @@ -504,10 +504,10 @@ public class AddContentApi( /** * Set the flag using flag code for a given note * @param noteId the ID of the note to update - * @param rawFlag the flag code in the range from 0 to 7 to set + * @param rawFlag the flag code to set (should match a [Flag.code]) * @return true if flag was updated, otherwise false * @throws SecurityException if READ_WRITE_PERMISSION not granted (e.g. due to install order bug) - * @throws IllegalArgumentException if flag code not in the range from 0 to 7 + * @throws IllegalArgumentException if flag code is not a valid [Flag.code] */ public fun setFlagRaw( noteId: Long, @@ -547,7 +547,7 @@ public class AddContentApi( /** * Set the flag for a given note * @param noteId the ID of the note to update - * @param flag the flag to set + * @param flag the flag to set (should match a [Flag]) * @return true if flag was updated, otherwise false * @throws SecurityException if READ_WRITE_PERMISSION not granted (e.g. due to install order bug) */ diff --git a/api/src/main/java/com/ichi2/anki/api/Flag.kt b/api/src/main/java/com/ichi2/anki/api/Flag.kt index 81290339f714..c671888505d2 100644 --- a/api/src/main/java/com/ichi2/anki/api/Flag.kt +++ b/api/src/main/java/com/ichi2/anki/api/Flag.kt @@ -18,4 +18,10 @@ public enum class Flag( PINK(5), TURQUOISE(6), PURPLE(7), + ; + + public companion object { + public val MAX_CODE: Int = entries.maxOf { it.code } + public val MIN_CODE: Int = entries.minOf { it.code } + } } From 1027d7319e0eefb7c1b27dff2450ebce892ccb84 Mon Sep 17 00:00:00 2001 From: snazarenko Date: Tue, 14 Jul 2026 22:30:54 +0300 Subject: [PATCH 4/4] fix: drop annotations for test, and change a little testSetUserFlag structure --- .../ichi2/anki/tests/ContentProviderTest.kt | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) 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 295eaa3f38f7..d2c7146bf0ff 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -2599,9 +2599,6 @@ class ContentProviderTest : InstrumentedTest() { } } - /** - * Check that set the flag in card works as expected - */ @Test fun testSetFlagCard() { val noteId = createdNotes.first().lastPathSegment!!.toLong() @@ -2647,9 +2644,6 @@ class ContentProviderTest : InstrumentedTest() { } } - /** - * Check that setUserFlag change only first 3 bits of flags - */ @Test fun testSetUserFlag() { val noteId = createdNotes.first().lastPathSegment!!.toLong() @@ -2658,14 +2652,19 @@ class ContentProviderTest : InstrumentedTest() { val card = col.getNote(noteId).cards(col).single() val noteCardUri = Uri.withAppendedPath(noteCardsUri, card.ord.toString()) - // set 8 (1000 in binary) manually + // Set 8 (1000 in binary) manually card.flags = 8 col.updateCard(card) - // set 4 (100 in binary) using setUserFlag method + // Set 4 (100 in binary) using setUserFlag method card.setUserFlag(4) col.updateCard(card) + // If we override only first 3 bits, we won't affect fourth. + // Result will be 12 (1000 | 0100 = 1100 in binary). + val updatedCard = col.getNote(noteId).cards(col).single() + assertEquals(12, updatedCard.flags) + val cursor = checkNotNull( contentResolver.query( @@ -2677,12 +2676,7 @@ class ContentProviderTest : InstrumentedTest() { ), ) { "cursor from /notes/#/cards/#" } - // If we override only first 3 bits, we won't affect fourth. - // Result will be 12 (1000 | 0100 = 1100 in binary). - val updatedCard = col.getNote(noteId).cards(col).single() - assertEquals(12, updatedCard.flags) - - // But cursor should return only first 3 bits + // But cursor should return 4 (only first 3 bits) cursor.use { assertTrue(it.moveToFirst()) assertEquals(4, it.getInt(it.getColumnIndex(FlashCardsContract.Card.FLAGS)))