Skip to content

Commit c84f993

Browse files
ujjol1234david-allison
authored andcommitted
fix(api): cards - list media
Fixed bug which was causing AnkiDroid API to not list media on cards. Due to card rendering being moved to the backend, and Anki using different Regex handling code, we needed to change the implementation of `files_in_str`: [sound:] tags were stripped, for example https://github.com/ankitects/anki/blob/64ca90934bc26ddf7125913abc9dd9de8cb30c2b/pylib/anki/media.py#L136-L150 Fixes 17062
1 parent 1d338b3 commit c84f993

File tree

4 files changed

+114
-11
lines changed

4 files changed

+114
-11
lines changed

AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package com.ichi2.anki.tests
2121
import android.content.ContentResolver
2222
import android.content.ContentUris
2323
import android.content.ContentValues
24+
import android.database.Cursor
2425
import android.database.CursorWindow
2526
import android.net.Uri
2627
import anki.notetypes.StockNotetype
@@ -49,12 +50,15 @@ import com.ichi2.libanki.getStockNotetype
4950
import com.ichi2.libanki.sched.Scheduler
5051
import com.ichi2.testutils.common.assertThrows
5152
import com.ichi2.utils.emptyStringArray
53+
import kotlinx.serialization.json.Json
5254
import net.ankiweb.rsdroid.exceptions.BackendNotFoundException
5355
import org.hamcrest.MatcherAssert.assertThat
56+
import org.hamcrest.Matchers.allOf
5457
import org.hamcrest.Matchers.containsString
5558
import org.hamcrest.Matchers.equalTo
5659
import org.hamcrest.Matchers.greaterThan
5760
import org.hamcrest.Matchers.greaterThanOrEqualTo
61+
import org.hamcrest.Matchers.hasItem
5862
import org.json.JSONObject.NULL
5963
import org.junit.After
6064
import org.junit.Assert.assertEquals
@@ -67,6 +71,7 @@ import org.junit.Rule
6771
import org.junit.Test
6872
import timber.log.Timber
6973
import kotlin.test.assertNotNull
74+
import kotlin.test.assertTrue
7075
import kotlin.test.junit.JUnitAsserter.assertNotNull
7176

7277
/**
@@ -1345,6 +1350,48 @@ class ContentProviderTest : InstrumentedTest() {
13451350
} ?: fail("query returned null")
13461351
}
13471352

1353+
@Test
1354+
fun testMediaFilesAddedCorrectlyInReviewInfo() {
1355+
val imageFileName = "img.jpg"
1356+
val audioFileName = "test.mp3"
1357+
addNoteUsingBasicNoteType("""Hello <img src="$imageFileName"> [sound:$audioFileName]""")
1358+
.firstCard(col)
1359+
.update {
1360+
queue = QueueType.New
1361+
due = col.sched.today
1362+
}
1363+
1364+
queryReviewInfo { cursor ->
1365+
val media =
1366+
cursor
1367+
.getString(cursor.getColumnIndex(FlashCardsContract.ReviewInfo.MEDIA_FILES))
1368+
.let { Json.decodeFromString<List<String>>(it) }
1369+
1370+
assertThat(
1371+
"media files returned",
1372+
media,
1373+
allOf(
1374+
hasItem(imageFileName),
1375+
hasItem(audioFileName),
1376+
),
1377+
)
1378+
}
1379+
}
1380+
1381+
private fun queryReviewInfo(block: (Cursor) -> Unit) {
1382+
contentResolver
1383+
.query(
1384+
FlashCardsContract.ReviewInfo.CONTENT_URI,
1385+
null,
1386+
null,
1387+
null,
1388+
null,
1389+
)?.use { cursor ->
1390+
assertTrue("has rows") { cursor.moveToFirst() }
1391+
block(cursor)
1392+
}
1393+
}
1394+
13481395
private fun reopenCol(): com.ichi2.libanki.Collection {
13491396
Timber.i("closeCollection: %s", "ContentProviderTest: reopenCol")
13501397
CollectionManager.closeCollectionBlocking()

AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ abstract class InstrumentedTest {
149149
}
150150
}
151151

152+
@DuplicatedCode("This should be refactored into a shared library later")
153+
fun Card.update(block: Card.() -> Unit): Card {
154+
block(this)
155+
col.updateCard(this)
156+
return this
157+
}
158+
152159
@DuplicatedCode("This is copied from RobolectricTest. This will be refactored into a shared library later")
153160
protected fun Card.moveToReviewQueue() {
154161
this.queue = QueueType.Rev

AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1137,7 +1137,7 @@ class CardContentProvider : ContentProvider() {
11371137
FlashCardsContract.ReviewInfo.NEXT_REVIEW_TIMES -> rb.add(nextReviewTimesJson.toString())
11381138
FlashCardsContract.ReviewInfo.MEDIA_FILES ->
11391139
rb.add(
1140-
JSONArray(col.media.filesInStr(currentCard.question(col) + currentCard.answer(col))),
1140+
JSONArray(col.media.filesInStr(currentCard.renderOutput(col))),
11411141
)
11421142
else -> throw UnsupportedOperationException("Queue \"$column\" is unknown")
11431143
}

AnkiDroid/src/main/java/com/ichi2/libanki/Media.kt

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ package com.ichi2.libanki
2020
import androidx.annotation.WorkerThread
2121
import anki.media.CheckMediaResponse
2222
import com.google.protobuf.kotlin.toByteString
23+
import com.ichi2.libanki.TemplateManager.TemplateRenderContext.TemplateRenderOutput
2324
import com.ichi2.libanki.exception.EmptyMediaException
25+
import com.ichi2.libanki.utils.LibAnkiAlias
26+
import com.ichi2.libanki.utils.NotInLibAnki
2427
import timber.log.Timber
2528
import java.io.File
2629

@@ -63,18 +66,36 @@ open class Media(
6366
/**
6467
* Extract media filenames from an HTML string.
6568
*
66-
* @param string The string to scan for media filenames ([sound:...] or <img...>).
67-
* @return A list containing all the sound and image filenames found in the input string.
69+
* @param currentCard The card to scan for media filenames ([sound:...] or <img...>).
70+
* @return A distinct, unordered list containing all the sound and image filenames found in the card.
6871
*/
69-
fun filesInStr(string: String): List<String> =
70-
col.backend
71-
.extractAvTags(string, true)
72-
.avTagsList
73-
.filter {
74-
it.hasSoundOrVideo()
75-
}.map {
76-
it.soundOrVideo
72+
@LibAnkiAlias("files_in_str")
73+
fun filesInStr(
74+
renderOutput: TemplateRenderOutput,
75+
includeRemote: Boolean = false,
76+
): List<String> {
77+
val files = mutableListOf<String>()
78+
val processedText = LaTeX.mungeQA(renderOutput.questionText + renderOutput.answerText, col, true)
79+
80+
for (pattern in htmlMediaRegexps) {
81+
val matches = pattern.findAll(processedText)
82+
for (match in matches) {
83+
val fname = pattern.extractFilename(match) ?: continue
84+
val isLocal = !Regex("(?i)https?|ftp://").containsMatchIn(fname)
85+
if (isLocal || includeRemote) {
86+
files.add(fname)
87+
}
7788
}
89+
}
90+
91+
// not in libAnki: the rendered output no longer contains [sound:] tags
92+
files.addAll(
93+
(renderOutput.questionAvTags + renderOutput.answerAvTags)
94+
.filterIsInstance<SoundOrVideoTag>()
95+
.map { it.filename },
96+
)
97+
return files.distinct()
98+
}
7899

79100
fun findUnusedMediaFiles(): List<File> = check().unusedList.map { File(dir, it) }
80101

@@ -134,4 +155,32 @@ open class Media(
134155
private fun restoreTrash() {
135156
col.backend.restoreTrash()
136157
}
158+
159+
companion object {
160+
/**
161+
* Given a media regex, return the index of the filename in the result
162+
*/
163+
@NotInLibAnki
164+
private fun Regex.extractFilename(match: MatchResult): String? {
165+
val index =
166+
when (htmlMediaRegexps.indexOf(this)) {
167+
0, 2 -> 3
168+
1, 3 -> 2
169+
else -> throw IllegalStateException(pattern)
170+
}
171+
return match.groups[index]?.value
172+
}
173+
174+
val htmlMediaRegexps =
175+
listOf(
176+
// src element quoted case (img/audio)
177+
Regex("(?i)(<(?:img|audio)\\b[^>]* src=(['\"])([^>]+?)\\2[^>]*>)"), // Group 3 = fname
178+
// unquoted src (img/audio)
179+
Regex("(?i)(<(?:img|audio)\\b[^>]* src=(?!['\"])([^ >]+)[^>]*?>)"), // Group 2 = fname
180+
// quoted data attribute (object)
181+
Regex("(?i)(<object\\b[^>]* data=(['\"])([^>]+?)\\2[^>]*>)"), // Group 3 = fname
182+
// unquoted data attribute (object)
183+
Regex("(?i)(<object\\b[^>]* data=(?!['\"])([^ >]+)[^>]*?>)"), // Group 2 = fname
184+
)
185+
}
137186
}

0 commit comments

Comments
 (0)