diff --git a/AnkiDroid/src/main/assets/card_template.html b/AnkiDroid/src/main/assets/card_template.html
index 899e0ea77076..ea9fe4613cae 100644
--- a/AnkiDroid/src/main/assets/card_template.html
+++ b/AnkiDroid/src/main/assets/card_template.html
@@ -13,6 +13,7 @@
+
diff --git a/AnkiDroid/src/main/assets/scripts/card.js b/AnkiDroid/src/main/assets/scripts/card.js
index 5747788a5520..86a1b0a79a2c 100644
--- a/AnkiDroid/src/main/assets/scripts/card.js
+++ b/AnkiDroid/src/main/assets/scripts/card.js
@@ -108,53 +108,6 @@ function reloadPage() {
window.location.href = "signal:reload_card_html";
}
-// Mark current card
-function ankiMarkCard() {
- window.location.href = "signal:mark_current_card";
-}
-
-/* Toggle flag on card from AnkiDroid Webview using JavaScript
- Possible values: "none", "red", "orange", "green", "blue"
- See AnkiDroid Manual for Usage
-*/
-function ankiToggleFlag(flag) {
- var flagVal = Number.isInteger(flag);
-
- if (flagVal) {
- switch (flag) {
- case 0:
- window.location.href = "signal:flag_none";
- break;
- case 1:
- window.location.href = "signal:flag_red";
- break;
- case 2:
- window.location.href = "signal:flag_orange";
- break;
- case 3:
- window.location.href = "signal:flag_green";
- break;
- case 4:
- window.location.href = "signal:flag_blue";
- break;
- case 5:
- window.location.href = "signal:flag_pink";
- break;
- case 6:
- window.location.href = "signal:flag_turquoise";
- break;
- case 7:
- window.location.href = "signal:flag_purple";
- break;
- default:
- console.log("No Flag Found");
- break;
- }
- } else {
- window.location.href = "signal:flag_" + flag;
- }
-}
-
// Show toast using js
function ankiShowToast(message) {
var msg = encodeURI(message);
diff --git a/AnkiDroid/src/main/assets/scripts/js-api.js b/AnkiDroid/src/main/assets/scripts/js-api.js
new file mode 100644
index 000000000000..780858cee55b
--- /dev/null
+++ b/AnkiDroid/src/main/assets/scripts/js-api.js
@@ -0,0 +1,120 @@
+/*
+ * AnkiDroid JavaScript API
+ * Version: 0.0.2
+ */
+
+/**
+ * jsApiList
+ *
+ * name: method name
+ * value: endpoint
+ */
+const jsApiList = {
+ ankiGetNewCardCount: "newCardCount",
+ ankiGetLrnCardCount: "lrnCardCount",
+ ankiGetRevCardCount: "revCardCount",
+ ankiGetETA: "eta",
+ ankiGetCardMark: "cardMark",
+ ankiGetCardFlag: "cardFlag",
+ ankiGetNextTime1: "nextTime1",
+ ankiGetNextTime2: "nextTime2",
+ ankiGetNextTime3: "nextTime3",
+ ankiGetNextTime4: "nextTime4",
+ ankiGetCardReps: "cardReps",
+ ankiGetCardInterval: "cardInterval",
+ ankiGetCardFactor: "cardFactor",
+ ankiGetCardMod: "cardMod",
+ ankiGetCardId: "cardId",
+ ankiGetCardNid: "cardNid",
+ ankiGetCardType: "cardType",
+ ankiGetCardDid: "cardDid",
+ ankiGetCardLeft: "cardLeft",
+ ankiGetCardODid: "cardODid",
+ ankiGetCardODue: "cardODue",
+ ankiGetCardQueue: "cardQueue",
+ ankiGetCardLapses: "cardLapses",
+ ankiGetCardDue: "cardDue",
+ ankiIsInFullscreen: "isInFullscreen",
+ ankiIsTopbarShown: "isTopbarShown",
+ ankiIsInNightMode: "isInNightMode",
+ ankiIsDisplayingAnswer: "isDisplayingAnswer",
+ ankiGetDeckName: "deckName",
+ ankiIsActiveNetworkMetered: "isActiveNetworkMetered",
+ ankiTtsFieldModifierIsAvailable: "ttsFieldModifierIsAvailable",
+ ankiTtsIsSpeaking: "ttsIsSpeaking",
+ ankiTtsStop: "ttsStop",
+ ankiBuryCard: "buryCard",
+ ankiBuryNote: "buryNote",
+ ankiSuspendCard: "suspendCard",
+ ankiSuspendNote: "suspendNote",
+ ankiAddTagToCard: "addTagToCard",
+ ankiResetProgress: "resetProgress",
+ ankiMarkCard: "markCard",
+ ankiToggleFlag: "toggleFlag",
+ ankiSearchCard: "searchCard",
+ ankiSearchCardWithCallback: "searchCardWithCallback",
+ ankiTtsSpeak: "ttsSpeak",
+ ankiTtsSetLanguage: "ttsSetLanguage",
+ ankiTtsSetPitch: "ttsSetPitch",
+ ankiTtsSetSpeechRate: "ttsSetSpeechRate",
+ ankiEnableHorizontalScrollbar: "enableHorizontalScrollbar",
+ ankiEnableVerticalScrollbar: "enableVerticalScrollbar",
+ ankiSetCardDue: "setCardDue",
+};
+
+class AnkiDroidJS {
+ constructor({ developer, version }) {
+ this.developer = developer;
+ this.version = version;
+ this.handleRequest(`init`);
+ }
+
+ static init({ developer, version }) {
+ return new AnkiDroidJS({ developer, version });
+ }
+
+ handleRequest = async (endpoint, data) => {
+ const url = `/jsapi/${endpoint}`;
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ developer: this.developer,
+ version: this.version,
+ data,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to make the request");
+ }
+
+ const responseData = await response.text();
+ if (endpoint.includes("nextTime") || endpoint.includes("deckName")) {
+ return responseData;
+ }
+ return JSON.parse(responseData);
+ } catch (error) {
+ console.error("Request error:", error);
+ throw error;
+ }
+ };
+}
+
+Object.keys(jsApiList).forEach(method => {
+ if (method === "ankiTtsSpeak") {
+ AnkiDroidJS.prototype[method] = async function (text, queueMode = 0) {
+ const endpoint = jsApiList[method];
+ const data = JSON.stringify({ text, queueMode });
+ return await this.handleRequest(endpoint, data);
+ };
+ return;
+ }
+ AnkiDroidJS.prototype[method] = async function (data) {
+ const endpoint = jsApiList[method];
+ return await this.handleRequest(endpoint, data);
+ };
+});
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
index cf0ff0b1fc44..018df181c2da 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
@@ -1053,7 +1053,6 @@ abstract class AbstractFlashcardViewer :
// Javascript interface for calling AnkiDroid functions in webview, see card.js
mAnkiDroidJsAPI = javaScriptFunction()
- webView.addJavascriptInterface(mAnkiDroidJsAPI!!, "AnkiDroidJS")
// enable dom storage so that sessionStorage & localStorage can be used in webview
webView.settings.domStorageEnabled = true
@@ -1313,9 +1312,6 @@ abstract class AbstractFlashcardViewer :
open fun displayCardQuestion() {
displayCardQuestion(false)
-
- // js api initialisation / reset
- mAnkiDroidJsAPI!!.init()
}
private fun displayCardQuestion(reload: Boolean) {
@@ -2295,58 +2291,6 @@ abstract class AbstractFlashcardViewer :
redrawCard()
return true
}
- // mark card using javascript
- if (url.startsWith("signal:mark_current_card")) {
- if (!mAnkiDroidJsAPI!!.isInit(AnkiDroidJsAPIConstants.MARK_CARD, AnkiDroidJsAPIConstants.ankiJsErrorCodeMarkCard)) {
- return true
- }
- executeCommand(ViewerCommand.MARK)
- return true
- }
- // flag card (blue, green, orange, red) using javascript from AnkiDroid webview
- if (url.startsWith("signal:flag_")) {
- if (!mAnkiDroidJsAPI!!.isInit(AnkiDroidJsAPIConstants.TOGGLE_FLAG, AnkiDroidJsAPIConstants.ankiJsErrorCodeFlagCard)) {
- return true
- }
- return when (url.replaceFirst("signal:flag_".toRegex(), "")) {
- "none" -> {
- executeCommand(ViewerCommand.UNSET_FLAG)
- true
- }
- "red" -> {
- executeCommand(ViewerCommand.TOGGLE_FLAG_RED)
- true
- }
- "orange" -> {
- executeCommand(ViewerCommand.TOGGLE_FLAG_ORANGE)
- true
- }
- "green" -> {
- executeCommand(ViewerCommand.TOGGLE_FLAG_GREEN)
- true
- }
- "blue" -> {
- executeCommand(ViewerCommand.TOGGLE_FLAG_BLUE)
- true
- }
- "pink" -> {
- executeCommand(ViewerCommand.TOGGLE_FLAG_PINK)
- true
- }
- "turquoise" -> {
- executeCommand(ViewerCommand.TOGGLE_FLAG_TURQUOISE)
- true
- }
- "purple" -> {
- executeCommand(ViewerCommand.TOGGLE_FLAG_PURPLE)
- true
- }
- else -> {
- Timber.d("No such Flag found.")
- true
- }
- }
- }
// Show toast using JS
if (url.startsWith("signal:anki_show_toast:")) {
@@ -2555,7 +2499,7 @@ abstract class AbstractFlashcardViewer :
}
}
- open fun javaScriptFunction(): AnkiDroidJsAPI? {
+ open fun javaScriptFunction(): AnkiDroidJsAPI {
return AnkiDroidJsAPI(this)
}
@@ -2565,6 +2509,10 @@ abstract class AbstractFlashcardViewer :
refreshIfRequired()
}
+ open fun getCardDataForJsApi(): AnkiDroidJsAPI.CardDataForJsApi {
+ return AnkiDroidJsAPI.CardDataForJsApi()
+ }
+
companion object {
/**
* Result codes that are returned when this activity finishes.
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt
index 25f7e81ceb23..6227d7840357 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt
@@ -22,75 +22,84 @@ package com.ichi2.anki
import android.content.Context
import android.content.Intent
import android.net.Uri
-import android.webkit.JavascriptInterface
import com.github.zafarkhaja.semver.Version
import com.google.android.material.snackbar.Snackbar
import com.ichi2.anim.ActivityTransitionAnimation
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryCard
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryNote
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeError
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeFlagCard
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeMarkCard
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeSetDue
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendCard
+import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendNote
+import com.ichi2.anki.AnkiDroidJsAPIConstants.flagCommands
+import com.ichi2.anki.cardviewer.ViewerCommand
import com.ichi2.anki.model.CardsOrNotes
+import com.ichi2.anki.servicelayer.rescheduleCards
+import com.ichi2.anki.servicelayer.resetCards
import com.ichi2.anki.snackbar.setMaxLines
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.libanki.Card
-import com.ichi2.libanki.CardId
-import com.ichi2.libanki.Consts.CARD_QUEUE
-import com.ichi2.libanki.Consts.CARD_TYPE
import com.ichi2.libanki.Decks
import com.ichi2.libanki.SortOrder
import com.ichi2.utils.NetworkUtils
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
-@Suppress("unused")
open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) {
private val currentCard: Card
get() = activity.currentCard!!
/**
Javascript Interface class for calling Java function from AnkiDroid WebView
- see card.js for available functions
+ see js-api.js for available functions
*/
private val context: Context = activity
- private var cardSuppliedDeveloperContact = ""
- private var cardSuppliedApiVersion = ""
-
- // JS api list enable/disable status
- private var mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap()
// Text to speech
private val mTalker = JavaScriptTTS()
- // init or reset api list
- fun init() {
- cardSuppliedApiVersion = ""
- cardSuppliedDeveloperContact = ""
- mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap()
+ open fun convertToByteArray(apiContract: ApiContract, boolean: Boolean): ByteArray {
+ return ApiResult(apiContract.isValid, boolean.toString()).toString().toByteArray()
+ }
+
+ open fun convertToByteArray(apiContract: ApiContract, int: Int): ByteArray {
+ return ApiResult(apiContract.isValid, int.toString()).toString().toByteArray()
+ }
+
+ open fun convertToByteArray(apiContract: ApiContract, long: Long): ByteArray {
+ return ApiResult(apiContract.isValid, long.toString()).toString().toByteArray()
}
- // Check if value null
- private fun isAnkiApiNull(api: String): Boolean {
- return mJsApiListMap[api] == null
+ open fun convertToByteArray(apiContract: ApiContract, string: String): ByteArray {
+ return ApiResult(apiContract.isValid, string).toString().toByteArray()
}
/**
- * Before calling js api check it init or not. It requires api name its error code.
- * If developer contract provided with correct js api version then it returns true
- *
- *
- * @param apiName
- * @param apiErrorCode
+ * The method parse json data and return api contract object
+ * @param byteArray
+ * @return apiContract or null
*/
- fun isInit(apiName: String, apiErrorCode: Int): Boolean {
- if (isAnkiApiNull(apiName)) {
- showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault)
- return false
- } else if (!getJsApiListMap()?.get(apiName)!!) {
- // see 02-string.xml
- showDeveloperContact(apiErrorCode)
- return false
+ private fun parseJsApiContract(byteArray: ByteArray): ApiContract? {
+ try {
+ val data = JSONObject(byteArray.decodeToString())
+ val cardSuppliedApiVersion = data.optString("version", "")
+ val cardSuppliedDeveloperContact = data.optString("developer", "")
+ val cardSuppliedData = data.optString("data", "")
+ val isValid = requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact)
+ return ApiContract(isValid, cardSuppliedDeveloperContact, cardSuppliedData)
+ } catch (j: JSONException) {
+ Timber.w(j)
+ activity.runOnUiThread {
+ activity.showSnackbar(context.getString(R.string.invalid_json_data, j.localizedMessage))
+ }
}
- return true
+ return null
}
/*
@@ -101,9 +110,9 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) {
*
* show developer contact if js api used in card is deprecated
*/
- fun showDeveloperContact(errorCode: Int) {
+ 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, cardSuppliedDeveloperContact, errorMsg)
+ val snackbarMsg: String = context.getString(R.string.api_version_developer_contact, apiDevContact, errorMsg)
activity.showSnackbar(snackbarMsg, Snackbar.LENGTH_INDEFINITE) {
setMaxLines(3)
@@ -118,7 +127,10 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) {
*/
private fun requireApiVersion(apiVer: String, apiDevContact: String): Boolean {
try {
- if (apiDevContact.isEmpty()) {
+ if (apiDevContact.isEmpty() || apiVer.isEmpty()) {
+ activity.runOnUiThread {
+ activity.showSnackbar(context.getString(R.string.invalid_json_data, ""))
+ }
return false
}
val versionCurrent = Version.valueOf(AnkiDroidJsAPIConstants.sCurrentJsApiVersion)
@@ -135,13 +147,13 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) {
}
versionSupplied.lessThan(versionCurrent) -> {
activity.runOnUiThread {
- activity.showSnackbar(context.getString(R.string.update_js_api_version, cardSuppliedDeveloperContact))
+ activity.showSnackbar(context.getString(R.string.update_js_api_version, apiDevContact))
}
versionSupplied.greaterThanOrEqualTo(Version.valueOf(AnkiDroidJsAPIConstants.sMinimumJsApiVersion))
}
else -> {
activity.runOnUiThread {
- activity.showSnackbar(context.getString(R.string.valid_js_api_version, cardSuppliedDeveloperContact))
+ activity.showSnackbar(context.getString(R.string.valid_js_api_version, apiDevContact))
}
false
}
@@ -152,325 +164,158 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) {
return false
}
- // if supplied api version match then enable api
- private fun enableJsApi() {
- for (api in mJsApiListMap) {
- mJsApiListMap[api.key] = true
+ /**
+ * Handle js api request,
+ * some of the methods are overriden in Reviewer.kt and default values are returned.
+ * @param methodName
+ * @param bytes
+ * @param isReviewer
+ * @return
+ */
+ open suspend fun handleJsApiRequest(methodName: String, bytes: ByteArray, isReviewer: Boolean) = 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
+ // also other action will not be modified
+ if (!apiContract.isValid or !isReviewer) {
+ return@withContext convertToByteArray(apiContract, -1)
}
- }
-
- protected fun getJsApiListMap(): HashMap? {
- return mJsApiListMap
- }
- @JavascriptInterface
- fun init(jsonData: String): String {
- val data: JSONObject
- var apiStatusJson = ""
- try {
- data = JSONObject(jsonData)
- cardSuppliedApiVersion = data.optString("version", "")
- cardSuppliedDeveloperContact = data.optString("developer", "")
- if (requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact)) {
- enableJsApi()
+ val cardDataForJsAPI = activity.getCardDataForJsApi()
+ val apiParams = apiContract.cardSuppliedData
+
+ return@withContext when (methodName) {
+ "init" -> convertToByteArray(apiContract, true)
+ "newCardCount" -> convertToByteArray(apiContract, cardDataForJsAPI.newCardCount)
+ "lrnCardCount" -> convertToByteArray(apiContract, cardDataForJsAPI.lrnCardCount)
+ "revCardCount" -> convertToByteArray(apiContract, cardDataForJsAPI.revCardCount)
+ "eta" -> convertToByteArray(apiContract, cardDataForJsAPI.eta)
+ "nextTime1" -> convertToByteArray(apiContract, cardDataForJsAPI.nextTime1)
+ "nextTime2" -> convertToByteArray(apiContract, cardDataForJsAPI.nextTime2)
+ "nextTime3" -> convertToByteArray(apiContract, cardDataForJsAPI.nextTime3)
+ "nextTime4" -> convertToByteArray(apiContract, cardDataForJsAPI.nextTime4)
+ "toggleFlag" -> {
+ if (apiParams !in flagCommands) {
+ showDeveloperContact(ankiJsErrorCodeFlagCard, apiContract.cardSuppliedDeveloperContact)
+ return@withContext convertToByteArray(apiContract, false)
+ }
+ convertToByteArray(apiContract, activity.executeCommand(flagCommands[apiParams]!!))
}
- apiStatusJson = JSONObject(mJsApiListMap as Map).toString()
- } catch (j: JSONException) {
- Timber.w(j)
- activity.runOnUiThread {
- activity.showSnackbar(context.getString(R.string.invalid_json_data, j.localizedMessage))
+ "markCard" -> processAction({ activity.executeCommand(ViewerCommand.MARK) }, apiContract, ankiJsErrorCodeMarkCard, ::convertToByteArray)
+ "buryCard" -> processAction(activity::buryCard, apiContract, ankiJsErrorCodeBuryCard, ::convertToByteArray)
+ "buryNote" -> processAction(activity::buryNote, apiContract, ankiJsErrorCodeBuryNote, ::convertToByteArray)
+ "suspendCard" -> processAction(activity::suspendCard, apiContract, ankiJsErrorCodeSuspendCard, ::convertToByteArray)
+ "suspendNote" -> processAction(activity::suspendNote, apiContract, ankiJsErrorCodeSuspendNote, ::convertToByteArray)
+ "setCardDue" -> {
+ try {
+ val days = apiParams.toInt()
+ if (days < 0 || days > 9999) {
+ showDeveloperContact(ankiJsErrorCodeSetDue, apiContract.cardSuppliedDeveloperContact)
+ return@withContext convertToByteArray(apiContract, false)
+ }
+ activity.launchCatchingTask {
+ activity.rescheduleCards(listOf(currentCard.id), days)
+ }
+ return@withContext convertToByteArray(apiContract, true)
+ } catch (e: NumberFormatException) {
+ showDeveloperContact(ankiJsErrorCodeSetDue, apiContract.cardSuppliedDeveloperContact)
+ return@withContext convertToByteArray(apiContract, false)
+ }
+ }
+ "resetProgress" -> {
+ val cardIds = listOf(currentCard.id)
+ activity.launchCatchingTask { activity.resetCards(cardIds) }
+ convertToByteArray(apiContract, true)
+ }
+ "cardMark" -> convertToByteArray(apiContract, currentCard.note().hasTag("marked"))
+ "cardFlag" -> convertToByteArray(apiContract, currentCard.userFlag())
+ "cardReps" -> convertToByteArray(apiContract, currentCard.reps)
+ "cardInterval" -> convertToByteArray(apiContract, currentCard.ivl)
+ "cardFactor" -> convertToByteArray(apiContract, currentCard.factor)
+ "cardMod" -> convertToByteArray(apiContract, currentCard.mod)
+ "cardId" -> convertToByteArray(apiContract, currentCard.id)
+ "cardNid" -> convertToByteArray(apiContract, currentCard.nid)
+ "cardType" -> convertToByteArray(apiContract, currentCard.type)
+ "cardDid" -> convertToByteArray(apiContract, currentCard.did)
+ "cardLeft" -> convertToByteArray(apiContract, currentCard.left)
+ "cardODid" -> convertToByteArray(apiContract, currentCard.oDid)
+ "cardODue" -> convertToByteArray(apiContract, currentCard.oDue)
+ "cardQueue" -> convertToByteArray(apiContract, currentCard.queue)
+ "cardLapses" -> convertToByteArray(apiContract, currentCard.lapses)
+ "cardDue" -> convertToByteArray(apiContract, currentCard.due)
+ "deckName" -> convertToByteArray(apiContract, Decks.basename(activity.getColUnsafe.decks.name(currentCard.did)))
+ "isActiveNetworkMetered" -> convertToByteArray(apiContract, NetworkUtils.isActiveNetworkMetered())
+ "ttsSetLanguage" -> convertToByteArray(apiContract, mTalker.setLanguage(apiParams))
+ "ttsSpeak" -> {
+ val jsonObject = JSONObject(apiParams)
+ val text = jsonObject.getString("text")
+ val queueMode = jsonObject.getInt("queueMode")
+ convertToByteArray(apiContract, mTalker.speak(text, queueMode))
+ }
+ "ttsIsSpeaking" -> convertToByteArray(apiContract, mTalker.isSpeaking)
+ "ttsSetPitch" -> convertToByteArray(apiContract, mTalker.setPitch(apiParams.toFloat()))
+ "ttsSetSpeechRate" -> convertToByteArray(apiContract, mTalker.setSpeechRate(apiParams.toFloat()))
+ "ttsFieldModifierIsAvailable" -> {
+ // Know if {{tts}} is supported - issue #10443
+ // Return false for now
+ convertToByteArray(apiContract, false)
+ }
+ "ttsStop" -> convertToByteArray(apiContract, mTalker.stop())
+ "searchCard" -> {
+ val intent = Intent(context, CardBrowser::class.java).apply {
+ putExtra("currentCard", currentCard.id)
+ putExtra("search_query", apiParams)
+ }
+ activity.startActivityWithAnimation(intent, ActivityTransitionAnimation.Direction.START)
+ convertToByteArray(apiContract, true)
+ }
+ "searchCardWithCallback" -> ankiSearchCardWithCallback(apiContract)
+ "isDisplayingAnswer" -> convertToByteArray(apiContract, activity.isDisplayingAnswer)
+ "addTagToCard" -> {
+ activity.runOnUiThread { activity.showTagsDialog() }
+ convertToByteArray(apiContract, true)
+ }
+ "isInFullscreen" -> convertToByteArray(apiContract, activity.isFullscreen)
+ "isTopbarShown" -> convertToByteArray(apiContract, activity.prefShowTopbar)
+ "isInNightMode" -> convertToByteArray(apiContract, activity.isInNightMode)
+ "enableHorizontalScrollbar" -> {
+ activity.webView!!.isHorizontalScrollBarEnabled = apiParams.toBoolean()
+ convertToByteArray(apiContract, true)
+ }
+ "enableVerticalScrollbar" -> {
+ activity.webView!!.isVerticalScrollBarEnabled = apiParams.toBoolean()
+ convertToByteArray(apiContract, true)
+ }
+ else -> {
+ showDeveloperContact(ankiJsErrorCodeError, apiContract.cardSuppliedDeveloperContact)
+ throw Exception("unhandled request: $methodName")
}
}
- return apiStatusJson
- }
-
- // This method and the one belows return "default" values when there is no count nor ETA.
- // Javascript may expect ETA and Counts to be set, this ensure it does not bug too much by providing a value of correct type
- // but with a clearly incorrect value.
- // It's overridden in the Reviewer, where those values are actually defined.
- @JavascriptInterface
- open fun ankiGetNewCardCount(): String? {
- return "-1"
- }
-
- @JavascriptInterface
- open fun ankiGetLrnCardCount(): String? {
- return "-1"
- }
-
- @JavascriptInterface
- open fun ankiGetRevCardCount(): String? {
- return "-1"
- }
-
- @JavascriptInterface
- open fun ankiGetETA(): Int {
- return -1
- }
-
- @JavascriptInterface
- fun ankiGetCardMark(): Boolean {
- return currentCard.note().hasTag("marked")
- }
-
- @JavascriptInterface
- fun ankiGetCardFlag(): Int {
- return currentCard.userFlag()
- }
-
- // behavior change ankiGetNextTime1...4
- @JavascriptInterface
- open fun ankiGetNextTime1(): String {
- return activity.easeButton1!!.nextTime
- }
-
- @JavascriptInterface
- open fun ankiGetNextTime2(): String {
- return activity.easeButton2!!.nextTime
- }
-
- @JavascriptInterface
- open fun ankiGetNextTime3(): String {
- return activity.easeButton3!!.nextTime
- }
-
- @JavascriptInterface
- open fun ankiGetNextTime4(): String {
- return activity.easeButton4!!.nextTime
- }
-
- @JavascriptInterface
- fun ankiGetCardReps(): Int {
- return currentCard.reps
- }
-
- @JavascriptInterface
- fun ankiGetCardInterval(): Int {
- return currentCard.ivl
- }
-
- /** Returns the ease as an int (percentage * 10). Default: 2500 (250%). Minimum: 1300 (130%) */
- @JavascriptInterface
- fun ankiGetCardFactor(): Int {
- return currentCard.factor
- }
-
- /** Returns the last modified time as a Unix timestamp in seconds. Example: 1477384099 */
- @JavascriptInterface
- fun ankiGetCardMod(): Long {
- return currentCard.mod
- }
-
- /** Returns the ID of the card. Example: 1477380543053 */
- @JavascriptInterface
- fun ankiGetCardId(): Long {
- return currentCard.id
- }
-
- /** Returns the ID of the note which generated the card. Example: 1590418157630 */
- @JavascriptInterface
- fun ankiGetCardNid(): Long {
- return currentCard.nid
- }
-
- @JavascriptInterface
- @CARD_TYPE
- fun ankiGetCardType(): Int {
- return currentCard.type
- }
-
- /** Returns the ID of the deck which contains the card. Example: 1595967594978 */
- @JavascriptInterface
- fun ankiGetCardDid(): Long {
- return currentCard.did
- }
-
- @JavascriptInterface
- fun ankiGetCardLeft(): Int {
- return currentCard.left
- }
-
- /** Returns the ID of the home deck for the card if it is filtered, or 0 if not filtered. Example: 1595967594978 */
- @JavascriptInterface
- fun ankiGetCardODid(): Long {
- return currentCard.oDid
- }
-
- @JavascriptInterface
- fun ankiGetCardODue(): Long {
- return currentCard.oDue
- }
-
- @JavascriptInterface
- @CARD_QUEUE
- fun ankiGetCardQueue(): Int {
- return currentCard.queue
- }
-
- @JavascriptInterface
- fun ankiGetCardLapses(): Int {
- return currentCard.lapses
- }
-
- @JavascriptInterface
- fun ankiGetCardDue(): Long {
- return currentCard.due
- }
-
- @JavascriptInterface
- fun ankiIsInFullscreen(): Boolean {
- return activity.isFullscreen
- }
-
- @JavascriptInterface
- fun ankiIsTopbarShown(): Boolean {
- return activity.prefShowTopbar
- }
-
- @JavascriptInterface
- fun ankiIsInNightMode(): Boolean {
- return activity.isInNightMode
- }
-
- @JavascriptInterface
- fun ankiIsDisplayingAnswer(): Boolean {
- return activity.isDisplayingAnswer
- }
-
- @JavascriptInterface
- fun ankiGetDeckName(): String {
- return Decks.basename(activity.getColUnsafe.decks.name(currentCard.did))
- }
-
- @JavascriptInterface
- fun ankiBuryCard(): Boolean {
- if (!isInit(AnkiDroidJsAPIConstants.BURY_CARD, AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryCard)) {
- return false
- }
-
- return activity.buryCard()
- }
-
- @JavascriptInterface
- fun ankiBuryNote(): Boolean {
- if (!isInit(AnkiDroidJsAPIConstants.BURY_NOTE, AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryNote)) {
- return false
- }
-
- return activity.buryNote()
- }
-
- @JavascriptInterface
- fun ankiSuspendCard(): Boolean {
- if (!isInit(AnkiDroidJsAPIConstants.SUSPEND_CARD, AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendCard)) {
- return false
- }
-
- return activity.suspendCard()
}
- @JavascriptInterface
- fun ankiSuspendNote(): Boolean {
- if (!isInit(AnkiDroidJsAPIConstants.SUSPEND_NOTE, AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendNote)) {
- return false
+ private fun processAction(
+ action: () -> Boolean,
+ apiContract: ApiContract,
+ errorCode: Int,
+ conversion: (ApiContract, Boolean) -> ByteArray
+ ): ByteArray {
+ val status = action()
+ if (!status) {
+ showDeveloperContact(errorCode, apiContract.cardSuppliedDeveloperContact)
}
-
- return activity.suspendNote()
+ return conversion(apiContract, status)
}
- @JavascriptInterface
- fun ankiAddTagToCard() {
- activity.runOnUiThread { activity.showTagsDialog() }
- }
-
- @JavascriptInterface
- fun ankiSearchCard(query: String?) {
- val intent = Intent(context, CardBrowser::class.java)
- val currentCardId: CardId = currentCard.id
- intent.putExtra("currentCard", currentCardId)
- intent.putExtra("search_query", query)
- activity.startActivityWithAnimation(intent, ActivityTransitionAnimation.Direction.START)
- }
-
- @JavascriptInterface
- fun ankiIsActiveNetworkMetered(): Boolean {
- return NetworkUtils.isActiveNetworkMetered()
- }
-
- // Know if {{tts}} is supported - issue #10443
- // Return false for now
- @JavascriptInterface
- fun ankiTtsFieldModifierIsAvailable(): Boolean {
- return false
- }
-
- @JavascriptInterface
- fun ankiTtsSpeak(text: String?, queueMode: Int): Int {
- return mTalker.speak(text, queueMode)
- }
-
- @JavascriptInterface
- fun ankiTtsSpeak(text: String?): Int {
- return mTalker.speak(text)
- }
-
- @JavascriptInterface
- fun ankiTtsSetLanguage(loc: String): Int {
- return mTalker.setLanguage(loc)
- }
-
- @JavascriptInterface
- fun ankiTtsSetPitch(pitch: Float): Int {
- return mTalker.setPitch(pitch)
- }
-
- @JavascriptInterface
- fun ankiTtsSetPitch(pitch: Double): Int {
- return mTalker.setPitch(pitch.toFloat())
- }
-
- @JavascriptInterface
- fun ankiTtsSetSpeechRate(speechRate: Float): Int {
- return mTalker.setSpeechRate(speechRate)
- }
-
- @JavascriptInterface
- fun ankiTtsSetSpeechRate(speechRate: Double): Int {
- return mTalker.setSpeechRate(speechRate.toFloat())
- }
-
- @JavascriptInterface
- fun ankiTtsIsSpeaking(): Boolean {
- return mTalker.isSpeaking
- }
-
- @JavascriptInterface
- fun ankiTtsStop(): Int {
- return mTalker.stop()
- }
-
- @JavascriptInterface
- fun ankiEnableHorizontalScrollbar(scroll: Boolean) {
- activity.webView!!.isHorizontalScrollBarEnabled = scroll
- }
-
- @JavascriptInterface
- fun ankiEnableVerticalScrollbar(scroll: Boolean) {
- activity.webView!!.isVerticalScrollBarEnabled = scroll
- }
-
- @JavascriptInterface
- fun ankiSearchCardWithCallback(query: String) {
+ private suspend fun ankiSearchCardWithCallback(apiContract: ApiContract): ByteArray = withContext(Dispatchers.Main) {
val cards = try {
- runBlocking {
- searchForCards(query, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS)
- }
+ 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
)
- return
+ showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSearchCard, apiContract.cardSuppliedDeveloperContact)
+ return@withContext convertToByteArray(apiContract, false)
}
val searchResult: MutableList = ArrayList()
for (s in cards) {
@@ -493,23 +338,32 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) {
}
// quote result to prevent JSON injection attack
- val jsonEncodedString = org.json.JSONObject.quote(searchResult.toString())
+ val jsonEncodedString = JSONObject.quote(searchResult.toString())
activity.runOnUiThread {
activity.webView!!.evaluateJavascript("ankiSearchCard($jsonEncodedString)", null)
}
+ convertToByteArray(apiContract, true)
+ }
+
+ open class CardDataForJsApi {
+ var newCardCount = ""
+ var lrnCardCount = ""
+ var revCardCount = ""
+ var eta = -1
+ var nextTime1 = ""
+ var nextTime2 = ""
+ var nextTime3 = ""
+ var nextTime4 = ""
+ }
+
+ class ApiResult(private val status: Boolean, private val value: String) {
+ override fun toString(): String {
+ return JSONObject().apply {
+ put("success", status)
+ put("value", value)
+ }.toString()
+ }
}
- @JavascriptInterface
- open fun ankiSetCardDue(days: Int): Boolean {
- // the function is overridden in Reviewer.kt
- // it may be called in previewer so just return true value here
- return true
- }
-
- @JavascriptInterface
- open fun ankiResetProgress(): Boolean {
- // the function is overridden in Reviewer.kt
- // it may be called in previewer so just return true value here
- return true
- }
+ class ApiContract(val isValid: Boolean, val cardSuppliedDeveloperContact: String, val cardSuppliedData: String)
}
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt
index bf206890853a..1f32a17486e9 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt
@@ -18,8 +18,11 @@
package com.ichi2.anki
+import com.ichi2.anki.cardviewer.ViewerCommand
+
object AnkiDroidJsAPIConstants {
// JS API ERROR CODE
+ const val ankiJsErrorCodeError: Int = -1
const val ankiJsErrorCodeDefault: Int = 0
const val ankiJsErrorCodeMarkCard: Int = 1
const val ankiJsErrorCodeFlagCard: Int = 2
@@ -29,33 +32,20 @@ object AnkiDroidJsAPIConstants {
const val ankiJsErrorCodeBuryNote: Int = 5
const val ankiJsErrorCodeSuspendNote: Int = 6
const val ankiJsErrorCodeSetDue: Int = 7
+ const val ankiJsErrorCodeSearchCard: Int = 8
// js api developer contact
- const val sCurrentJsApiVersion = "0.0.1"
- const val sMinimumJsApiVersion = "0.0.1"
-
- const val MARK_CARD = "markCard"
- const val TOGGLE_FLAG = "toggleFlag"
-
- const val BURY_CARD = "buryCard"
- const val BURY_NOTE = "buryNote"
- const val SUSPEND_CARD = "suspendCard"
- const val SUSPEND_NOTE = "suspendNote"
- const val SET_CARD_DUE = "setCardDue"
- const val RESET_PROGRESS = "setCardDue"
-
- fun initApiMap(): HashMap {
- val jsApiListMap = HashMap()
- jsApiListMap[MARK_CARD] = false
- jsApiListMap[TOGGLE_FLAG] = false
-
- jsApiListMap[BURY_CARD] = false
- jsApiListMap[BURY_NOTE] = false
- jsApiListMap[SUSPEND_CARD] = false
- jsApiListMap[SUSPEND_NOTE] = false
- jsApiListMap[SET_CARD_DUE] = false
- jsApiListMap[RESET_PROGRESS] = false
-
- return jsApiListMap
- }
+ const val sCurrentJsApiVersion = "0.0.2"
+ const val sMinimumJsApiVersion = "0.0.2"
+
+ 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/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
index 4cec789d12b3..246763190dff 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
@@ -32,7 +32,6 @@ import android.os.Parcelable
import android.text.SpannableString
import android.text.style.UnderlineSpan
import android.view.*
-import android.webkit.JavascriptInterface
import android.widget.*
import androidx.annotation.*
import androidx.appcompat.app.AlertDialog
@@ -47,10 +46,6 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.ichi2.anim.ActivityTransitionAnimation
import com.ichi2.anim.ActivityTransitionAnimation.getInverseTransition
-import com.ichi2.anki.AnkiDroidJsAPIConstants.RESET_PROGRESS
-import com.ichi2.anki.AnkiDroidJsAPIConstants.SET_CARD_DUE
-import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault
-import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeSetDue
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.Whiteboard.Companion.createInstance
import com.ichi2.anki.Whiteboard.OnPaintColorChangeListener
@@ -1558,83 +1553,21 @@ open class Reviewer :
}
override fun javaScriptFunction(): AnkiDroidJsAPI {
- return ReviewerJavaScriptFunction(this)
+ return AnkiDroidJsAPI(this)
}
- inner class ReviewerJavaScriptFunction(activity: AbstractFlashcardViewer) : AnkiDroidJsAPI(activity) {
- @JavascriptInterface
- override fun ankiGetNewCardCount(): String {
- return mNewCount.toString()
- }
-
- @JavascriptInterface
- override fun ankiGetLrnCardCount(): String {
- return mLrnCount.toString()
- }
-
- @JavascriptInterface
- override fun ankiGetRevCardCount(): String {
- return mRevCount.toString()
- }
-
- @JavascriptInterface
- override fun ankiGetETA(): Int {
- return mEta
- }
-
- @JavascriptInterface
- override fun ankiGetNextTime1(): String {
- return easeButton1!!.nextTime
- }
-
- @JavascriptInterface
- override fun ankiGetNextTime2(): String {
- return easeButton2!!.nextTime
- }
-
- @JavascriptInterface
- override fun ankiGetNextTime3(): String {
- return easeButton3!!.nextTime
- }
-
- @JavascriptInterface
- override fun ankiGetNextTime4(): String {
- return easeButton4!!.nextTime
- }
-
- @JavascriptInterface
- override fun ankiSetCardDue(days: Int): Boolean {
- val apiList = getJsApiListMap()!!
- if (!apiList[SET_CARD_DUE]!!) {
- showDeveloperContact(ankiJsErrorCodeDefault)
- return false
- }
-
- if (days < 0 || days > 9999) {
- showDeveloperContact(ankiJsErrorCodeSetDue)
- return false
- }
-
- val cardIds = listOf(currentCard!!.id)
- launchCatchingTask {
- rescheduleCards(cardIds, days)
- }
- return true
- }
-
- @JavascriptInterface
- override fun ankiResetProgress(): Boolean {
- val apiList = getJsApiListMap()!!
- if (!apiList[RESET_PROGRESS]!!) {
- showDeveloperContact(ankiJsErrorCodeDefault)
- return false
- }
- val cardIds = listOf(currentCard!!.id)
- launchCatchingTask {
- resetCards(cardIds)
- }
- return true
+ override fun getCardDataForJsApi(): AnkiDroidJsAPI.CardDataForJsApi {
+ val cardDataForJsAPI = AnkiDroidJsAPI.CardDataForJsApi().apply {
+ newCardCount = mNewCount.toString()
+ lrnCardCount = mLrnCount.toString()
+ revCardCount = mRevCount.toString()
+ nextTime1 = easeButton1!!.nextTime
+ nextTime2 = easeButton2!!.nextTime
+ nextTime3 = easeButton3!!.nextTime
+ nextTime4 = easeButton4!!.nextTime
+ eta = mEta
}
+ return cardDataForJsAPI
}
companion object {
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt
index 9e4972bfc4dd..509a961375b6 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt
@@ -26,6 +26,11 @@ import java.io.FileInputStream
class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiServer(activity) {
var reviewerHtml: String = ""
+ private val jsApi = if (activity is Reviewer) {
+ reviewer().javaScriptFunction()
+ } else {
+ cardTemplatePreviewer().javaScriptFunction()
+ }
override fun start() {
super.start()
@@ -72,6 +77,11 @@ class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiSer
handlePostRequest(uri.substring(ANKI_PREFIX.length), inputBytes)
}
}
+ if (uri.startsWith(ANKIDROID_JS_PREFIX)) {
+ return buildResponse {
+ jsApi.handleJsApiRequest(uri.substring(ANKIDROID_JS_PREFIX.length), inputBytes, activity is Reviewer)
+ }
+ }
}
Timber.w("not found: $uri")
@@ -92,6 +102,10 @@ class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiSer
return (activity as Reviewer)
}
+ private fun cardTemplatePreviewer(): CardTemplatePreviewer {
+ return (activity as CardTemplatePreviewer)
+ }
+
private fun getSchedulingStatesWithContext(): ByteArray {
val state = reviewer().queueState ?: return ByteArray(0)
return state.schedulingStatesWithContext().toByteArray()
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
index 2612f7dadac5..7893e76f385b 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
@@ -120,6 +120,7 @@ open class AnkiServer(
companion object {
/** Common prefix used on Anki requests */
const val ANKI_PREFIX = "/_anki/"
+ const val ANKIDROID_JS_PREFIX = "/jsapi/"
fun getMimeFromUri(uri: String): String {
return when (uri.substringAfterLast(".")) {
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt
index d4506dc4f32b..772d9faacc99 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt
@@ -26,7 +26,7 @@ import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.json.JSONObject
import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,7 +34,7 @@ import org.junit.runner.RunWith
class AnkiDroidJsAPITest : RobolectricTest() {
@Test
- fun initTest() {
+ fun ankiGetNextTimeTest() = runTest {
val models = col.notetypes
val decks = col.decks
val didA = addDeck("Test")
@@ -44,45 +44,32 @@ class AnkiDroidJsAPITest : RobolectricTest() {
decks.select(didA)
val reviewer: Reviewer = startReviewer()
- val javaScriptFunction = reviewer.javaScriptFunction()
-
- val data = JSONObject()
- data.put("version", "0.0.1")
- data.put("developer", "dev@mail.com")
-
- // this will be changed when new api added
- // TODO - make this test to auto add api from list
- val expected = "{\"setCardDue\":true,\"suspendNote\":true,\"markCard\":true,\"suspendCard\":true,\"buryCard\":true,\"toggleFlag\":true,\"buryNote\":true}"
-
- waitForAsyncTasksToComplete()
- assertThat(javaScriptFunction.init(data.toString()), equalTo(expected))
- }
-
- @Test
- fun ankiGetNextTimeTest() {
- val models = col.notetypes
- val decks = col.decks
- val didA = addDeck("Test")
- val basic = models.byName(AnkiDroidApp.appResources.getString(R.string.basic_model_name))
- basic!!.put("did", didA)
- addNoteUsingBasicModel("foo", "bar")
- decks.select(didA)
-
- val reviewer: Reviewer = startReviewer()
- val javaScriptFunction = reviewer.javaScriptFunction()
+ val jsapi = reviewer.javaScriptFunction()
reviewer.displayCardAnswer()
waitForAsyncTasksToComplete()
- assertThat(javaScriptFunction.ankiGetNextTime1().withoutUnicodeIsolation(), equalTo("<1m"))
- assertThat(javaScriptFunction.ankiGetNextTime2().withoutUnicodeIsolation(), equalTo("<6m"))
- assertThat(javaScriptFunction.ankiGetNextTime3().withoutUnicodeIsolation(), equalTo("<10m"))
- assertThat(javaScriptFunction.ankiGetNextTime4().withoutUnicodeIsolation(), equalTo("4d"))
+ assertThat(
+ getDataFromRequest("nextTime1", jsapi).withoutUnicodeIsolation(),
+ equalTo(formatApiResult("<1m"))
+ )
+ assertThat(
+ getDataFromRequest("nextTime2", jsapi).withoutUnicodeIsolation(),
+ equalTo(formatApiResult("<6m"))
+ )
+ assertThat(
+ getDataFromRequest("nextTime3", jsapi).withoutUnicodeIsolation(),
+ equalTo(formatApiResult("<10m"))
+ )
+ assertThat(
+ getDataFromRequest("nextTime4", jsapi).withoutUnicodeIsolation(),
+ equalTo(formatApiResult("4d"))
+ )
}
@Test
- fun ankiTestCurrentCard() {
+ fun ankiTestCurrentCard() = runTest {
val models = col.notetypes
val decks = col.decks
val didA = addDeck("Test")
@@ -92,7 +79,7 @@ class AnkiDroidJsAPITest : RobolectricTest() {
decks.select(didA)
val reviewer: Reviewer = startReviewer()
- val javaScriptFunction = reviewer.javaScriptFunction()
+ val jsapi = reviewer.javaScriptFunction()
reviewer.displayCardAnswer()
waitForAsyncTasksToComplete()
@@ -100,47 +87,101 @@ class AnkiDroidJsAPITest : RobolectricTest() {
val currentCard = reviewer.currentCard!!
// Card Did
- assertThat(javaScriptFunction.ankiGetCardDid(), equalTo(currentCard.did))
+ assertThat(
+ getDataFromRequest("cardDid", jsapi),
+ equalTo(formatApiResult(currentCard.did))
+ )
// Card Id
- assertThat(javaScriptFunction.ankiGetCardId(), equalTo(currentCard.id))
+ assertThat(
+ getDataFromRequest("cardId", jsapi),
+ equalTo(formatApiResult(currentCard.id))
+ )
// Card Nid
- assertThat(javaScriptFunction.ankiGetCardNid(), equalTo(currentCard.nid))
+ assertThat(
+ getDataFromRequest("cardNid", jsapi),
+ equalTo(formatApiResult(currentCard.nid))
+ )
// Card ODid
- assertThat(javaScriptFunction.ankiGetCardODid(), equalTo(currentCard.oDid))
+ assertThat(
+ getDataFromRequest("cardODid", jsapi),
+ equalTo(formatApiResult(currentCard.oDid))
+ )
// Card Type
- assertThat(javaScriptFunction.ankiGetCardType(), equalTo(currentCard.type))
+ assertThat(
+ getDataFromRequest("cardType", jsapi),
+ equalTo(formatApiResult(currentCard.type))
+ )
// Card ODue
- assertThat(javaScriptFunction.ankiGetCardODue(), equalTo(currentCard.oDue))
+ assertThat(
+ getDataFromRequest("cardODue", jsapi),
+ equalTo(formatApiResult(currentCard.oDue))
+ )
// Card Due
- assertThat(javaScriptFunction.ankiGetCardDue(), equalTo(currentCard.due))
+ assertThat(
+ getDataFromRequest("cardDue", jsapi),
+ equalTo(formatApiResult(currentCard.due))
+ )
// Card Factor
- assertThat(javaScriptFunction.ankiGetCardFactor(), equalTo(currentCard.factor))
+ assertThat(
+ getDataFromRequest("cardFactor", jsapi),
+ equalTo(formatApiResult(currentCard.factor))
+ )
// Card Lapses
- assertThat(javaScriptFunction.ankiGetCardLapses(), equalTo(currentCard.lapses))
+ assertThat(
+ getDataFromRequest("cardLapses", jsapi),
+ equalTo(formatApiResult(currentCard.lapses))
+ )
// Card Ivl
- assertThat(javaScriptFunction.ankiGetCardInterval(), equalTo(currentCard.ivl))
+ assertThat(
+ getDataFromRequest("cardInterval", jsapi),
+ equalTo(formatApiResult(currentCard.ivl))
+ )
// Card mod
- assertThat(javaScriptFunction.ankiGetCardMod(), equalTo(currentCard.mod))
+ assertThat(
+ getDataFromRequest("cardMod", jsapi),
+ equalTo(formatApiResult(currentCard.mod))
+ )
// Card Queue
- assertThat(javaScriptFunction.ankiGetCardQueue(), equalTo(currentCard.queue))
+ assertThat(
+ getDataFromRequest("cardQueue", jsapi),
+ equalTo(formatApiResult(currentCard.queue))
+ )
// Card Reps
- assertThat(javaScriptFunction.ankiGetCardReps(), equalTo(currentCard.reps))
+ assertThat(
+ getDataFromRequest("cardReps", jsapi),
+ equalTo(formatApiResult(currentCard.reps))
+ )
// Card left
- assertThat(javaScriptFunction.ankiGetCardLeft(), equalTo(currentCard.left))
+ assertThat(
+ getDataFromRequest("cardLeft", jsapi),
+ equalTo(formatApiResult(currentCard.left))
+ )
// Card Flag
- assertThat(javaScriptFunction.ankiGetCardFlag(), equalTo(0))
+ assertThat(
+ getDataFromRequest("cardFlag", jsapi),
+ equalTo(formatApiResult(0))
+ )
reviewer.currentCard!!.setFlag(1)
- assertThat(javaScriptFunction.ankiGetCardFlag(), equalTo(1))
+ assertThat(
+ getDataFromRequest("cardFlag", jsapi),
+ equalTo(formatApiResult(1))
+ )
// Card Mark
- assertThat(javaScriptFunction.ankiGetCardMark(), equalTo(false))
+ assertThat(
+ getDataFromRequest("cardMark", jsapi),
+ equalTo(formatApiResult(false))
+ )
reviewer.currentCard!!.note().addTag("marked")
- assertThat(javaScriptFunction.ankiGetCardMark(), equalTo(true))
+ assertThat(
+ getDataFromRequest("cardMark", jsapi),
+ equalTo(formatApiResult(true))
+ )
}
@Test
- fun ankiJsUiTest() {
+ fun ankiJsUiTest() = runTest {
val models = col.notetypes
val decks = col.decks
val didA = addDeck("Test")
@@ -150,25 +191,40 @@ class AnkiDroidJsAPITest : RobolectricTest() {
decks.select(didA)
val reviewer: Reviewer = startReviewer()
- val javaScriptFunction = reviewer.javaScriptFunction()
+ val jsapi = reviewer.javaScriptFunction()
waitForAsyncTasksToComplete()
// Displaying question
- assertThat(javaScriptFunction.ankiIsDisplayingAnswer(), equalTo(reviewer.isDisplayingAnswer))
+ assertThat(
+ getDataFromRequest("isDisplayingAnswer", jsapi),
+ equalTo(formatApiResult(reviewer.isDisplayingAnswer))
+ )
reviewer.displayCardAnswer()
- assertThat(javaScriptFunction.ankiIsDisplayingAnswer(), equalTo(reviewer.isDisplayingAnswer))
+ assertThat(
+ getDataFromRequest("isDisplayingAnswer", jsapi),
+ equalTo(formatApiResult(reviewer.isDisplayingAnswer))
+ )
// Full Screen
- assertThat(javaScriptFunction.ankiIsInFullscreen(), equalTo(reviewer.isFullscreen))
+ assertThat(
+ getDataFromRequest("isInFullscreen", jsapi),
+ equalTo(formatApiResult(reviewer.isFullscreen))
+ )
// Top bar
- assertThat(javaScriptFunction.ankiIsTopbarShown(), equalTo(reviewer.prefShowTopbar))
+ assertThat(
+ getDataFromRequest("isTopbarShown", jsapi),
+ equalTo(formatApiResult(reviewer.prefShowTopbar))
+ )
// Night Mode
- assertThat(javaScriptFunction.ankiIsInNightMode(), equalTo(reviewer.isInNightMode))
+ assertThat(
+ getDataFromRequest("isInNightMode", jsapi),
+ equalTo(formatApiResult(reviewer.isInNightMode))
+ )
}
@Test
- fun ankiMarkAndFlagCardTest() {
+ fun ankiMarkAndFlagCardTest() = runTest {
// js api test for marking and flagging card
val models = col.notetypes
val decks = col.decks
@@ -179,7 +235,7 @@ class AnkiDroidJsAPITest : RobolectricTest() {
decks.select(didA)
val reviewer: Reviewer = startReviewer()
- val javaScriptFunction = reviewer.javaScriptFunction()
+ val jsapi = reviewer.javaScriptFunction()
waitForAsyncTasksToComplete()
@@ -187,52 +243,47 @@ class AnkiDroidJsAPITest : RobolectricTest() {
// Card mark test
// ---------------
// Before marking card
- assertThat(javaScriptFunction.ankiGetCardMark(), equalTo(false))
-
- // call javascript function defined in card.js to mark card
- var markCardJs = "javascript:(function () {\n"
-
- // add js api developer contract
- markCardJs += "var jsApi = {\"version\" : \"0.0.1\", \"developer\" : \"dev@mail.com\"};\n"
-
- // init JS API
- markCardJs += "AnkiDroidJS.init(JSON.stringify(jsApi));\n"
-
- // call function defined in card.js to mark card
- markCardJs += "ankiMarkCard();\n"
-
- // get card mark status for test
- markCardJs += "AnkiDroidJS.ankiGetCardMark();\n" +
- "})();"
-
- reviewer.webView!!.evaluateJavascript(markCardJs) { s -> assertThat(s, equalTo(true)) }
+ assertThat(
+ getDataFromRequest("cardMark", jsapi),
+ equalTo(formatApiResult(false))
+ )
+
+ // Mark card
+ assertThat(
+ getDataFromRequest("markCard", jsapi, "true"),
+ equalTo(formatApiResult(true))
+ )
+
+ // After marking card
+ assertThat(
+ getDataFromRequest("cardMark", jsapi),
+ equalTo(formatApiResult(true))
+ )
// ---------------
// Card flag test
// ---------------
// before toggling flag
- assertThat(javaScriptFunction.ankiGetCardFlag(), equalTo(0))
-
- // call javascript function defined in card.js to toggle flag
- var flagCardJs = "javascript:(function () {\n"
-
- // add js api developer contract
- flagCardJs += "var jsApi = {\"version\" : \"0.0.1\", \"developer\" : \"test@example.com\"};\n"
-
- // init JS API
- flagCardJs += "AnkiDroidJS.init(JSON.stringify(jsApi));\n"
-
- // call function defined in card.js to flag card to red
- flagCardJs += "ankiToggleFlag(\"red\");\n"
-
- // get flag status for test
- flagCardJs += "AnkiDroidJS.ankiGetCardFlag();\n" +
- "})();"
-
- reviewer.webView!!.evaluateJavascript(flagCardJs) { s -> assertThat(s, equalTo(1)) }
+ assertThat(
+ getDataFromRequest("cardFlag", jsapi),
+ equalTo(formatApiResult(0))
+ )
+
+ // call javascript function to toggle flag
+ assertThat(
+ getDataFromRequest("toggleFlag", jsapi, "red"),
+ equalTo(formatApiResult(true))
+ )
+
+ // after toggling flag
+ assertThat(
+ getDataFromRequest("cardFlag", jsapi),
+ equalTo(formatApiResult(1))
+ )
}
- fun ankiBurySuspendTest() {
+ @Ignore("the test need to be updated")
+ fun ankiBurySuspendTest() = runTest {
// js api test for bury and suspend notes and cards
// add five notes, four will be buried and suspended
// count number of notes, if buried or suspended then
@@ -250,15 +301,16 @@ class AnkiDroidJsAPITest : RobolectricTest() {
decks.select(didA)
val reviewer: Reviewer = startReviewer()
-
- waitForAsyncTasksToComplete()
+ val jsapi = reviewer.javaScriptFunction()
// ----------
// Bury Card
// ----------
- var jsScript = createTestScript("AnkiDroidJS.ankiBuryCard();")
// call script to bury current card
- reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) }
+ assertThat(
+ getDataFromRequest("buryCard", jsapi),
+ equalTo(formatApiResult(true))
+ )
// count number of notes
val sched = reviewer.getColUnsafe
@@ -267,9 +319,11 @@ class AnkiDroidJsAPITest : RobolectricTest() {
// ----------
// Bury Note
// ----------
- jsScript = createTestScript("AnkiDroidJS.ankiBuryNote();")
// call script to bury current note
- reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) }
+ assertThat(
+ getDataFromRequest("buryNote", jsapi),
+ equalTo(formatApiResult(true))
+ )
// count number of notes
assertThat(sched.cardCount(), equalTo(3))
@@ -277,9 +331,11 @@ class AnkiDroidJsAPITest : RobolectricTest() {
// -------------
// Suspend Card
// -------------
- jsScript = createTestScript("AnkiDroidJS.ankiSuspendCard();")
// call script to suspend current card
- reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) }
+ assertThat(
+ getDataFromRequest("suspendCard", jsapi),
+ equalTo(formatApiResult(true))
+ )
// count number of notes
assertThat(sched.cardCount(), equalTo(2))
@@ -287,30 +343,16 @@ class AnkiDroidJsAPITest : RobolectricTest() {
// -------------
// Suspend Note
// -------------
- jsScript = createTestScript("AnkiDroidJS.ankiSuspendNote();")
// call script to suspend current note
- reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) }
+ assertThat(
+ getDataFromRequest("suspendNote", jsapi),
+ equalTo(formatApiResult(true))
+ )
// count number of notes
assertThat(sched.cardCount(), equalTo(1))
}
- private fun createTestScript(apiName: String): String {
- // create js script for evaluating in webview
- var script = "javascript:(function () {\n"
-
- // add js api developer contract
- script += "var jsApi = {\"version\" : \"0.0.1\", \"developer\" : \"test@example.com\"};\n"
-
- // init JS API
- script += "AnkiDroidJS.init(JSON.stringify(jsApi));\n"
-
- // call js api
- script += "$apiName\n})();"
-
- return script
- }
-
private fun startReviewer(): Reviewer {
return ReviewerTest.startReviewer(this)
}
@@ -330,27 +372,20 @@ class AnkiDroidJsAPITest : RobolectricTest() {
val reviewer: Reviewer = startReviewer()
waitForAsyncTasksToComplete()
- val javaScriptFunction = reviewer.javaScriptFunction()
- // init js api
- javaScriptFunction.init(initJsApiContract())
+ val jsapi = reviewer.javaScriptFunction()
// get card id for testing due
- val cardId = javaScriptFunction.ankiGetCardId()
+ val cardIdRes = getDataFromRequest("cardId", jsapi)
+ val jsonObject = JSONObject(cardIdRes)
+ val cardId = jsonObject.get("value").toString().toLong()
// test that card rescheduled for 15 days interval and returned true
- assertTrue("Card rescheduled, so returns true", javaScriptFunction.ankiSetCardDue(15))
+ assertThat(getDataFromRequest("setCardDue", jsapi, "15"), equalTo(formatApiResult(true)))
waitForAsyncTasksToComplete()
// verify that it did get rescheduled
// --------------------------------
- val cardAfterRescheduleCards = col.getCard(cardId)
- assertEquals("Card is rescheduled", 15L + col.sched.today, cardAfterRescheduleCards.due)
- }
-
- private fun initJsApiContract(): String {
- val data = JSONObject()
- data.put("version", "0.0.1")
- data.put("developer", "test@example.com")
- return data.toString()
+ val cardToBeReschedule = col.getCard(cardId)
+ assertEquals("Card is rescheduled", 15L + col.sched.today, cardToBeReschedule.due)
}
@Test
@@ -373,21 +408,40 @@ class AnkiDroidJsAPITest : RobolectricTest() {
val reviewer: Reviewer = startReviewer()
waitForAsyncTasksToComplete()
- val javaScriptFunction = reviewer.javaScriptFunction()
- // init js api
- javaScriptFunction.init(initJsApiContract())
- // get card id for testing due
- val cardId = javaScriptFunction.ankiGetCardId()
+ val jsapi = reviewer.javaScriptFunction()
// test that card reset
- assertTrue("Card progress reset", javaScriptFunction.ankiResetProgress())
+ assertThat(getDataFromRequest("resetProgress", jsapi), equalTo(formatApiResult(true)))
waitForAsyncTasksToComplete()
// verify that card progress reset
// --------------------------------
- val cardAfterReset = col.getCard(cardId)
+ val cardAfterReset = col.getCard(reviewer.currentCard!!.id)
assertEquals("Card due after reset", 2, cardAfterReset.due)
assertEquals("Card interval after reset", 0, cardAfterReset.ivl)
assertEquals("Card type after reset", Consts.CARD_TYPE_NEW, cardAfterReset.type)
}
+
+ companion object {
+ fun jsApiContract(data: String = ""): ByteArray {
+ return JSONObject().apply {
+ put("version", "0.0.2")
+ put("developer", "test@example.com")
+ put("data", data)
+ }.toString().toByteArray()
+ }
+
+ fun formatApiResult(res: Any): String {
+ return "{\"success\":true,\"value\":\"$res\"}"
+ }
+
+ suspend fun getDataFromRequest(
+ methodName: String,
+ jsAPI: AnkiDroidJsAPI,
+ apiData: String = ""
+ ): String {
+ return jsAPI.handleJsApiRequest(methodName, jsApiContract(apiData), true)
+ .decodeToString()
+ }
+ }
}
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt
index e468b678e33a..f75a98fff82a 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt
@@ -21,6 +21,9 @@ import androidx.core.content.edit
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.AbstractFlashcardViewer.Companion.RESULT_DEFAULT
+import com.ichi2.anki.AnkiDroidJsAPITest.Companion.formatApiResult
+import com.ichi2.anki.AnkiDroidJsAPITest.Companion.getDataFromRequest
+import com.ichi2.anki.AnkiDroidJsAPITest.Companion.jsApiContract
import com.ichi2.anki.cardviewer.ViewerCommand
import com.ichi2.anki.cardviewer.ViewerCommand.FLIP_OR_ANSWER_EASE1
import com.ichi2.anki.cardviewer.ViewerCommand.MARK
@@ -222,7 +225,7 @@ class ReviewerTest : RobolectricTest() {
}
@Test
- fun jsAnkiGetDeckName() {
+ fun jsAnkiGetDeckName() = runTest {
val models = col.notetypes
val decks = col.decks
@@ -238,7 +241,11 @@ class ReviewerTest : RobolectricTest() {
val javaScriptFunction = reviewer.javaScriptFunction()
waitForAsyncTasksToComplete()
- assertThat(javaScriptFunction.ankiGetDeckName(), equalTo("B"))
+ assertThat(
+ javaScriptFunction.handleJsApiRequest("deckName", jsApiContract(), true)
+ .decodeToString(),
+ equalTo(formatApiResult("B"))
+ )
}
@Ignore("needs update for v3")
@@ -324,20 +331,18 @@ class ReviewerTest : RobolectricTest() {
}
@Suppress("SameParameterValue")
- private fun assertCounts(r: Reviewer, newCount: Int, stepCount: Int, revCount: Int) {
+ private fun assertCounts(r: Reviewer, newCount: Int, stepCount: Int, revCount: Int) = runTest {
val jsApi = r.javaScriptFunction()
val countList = listOf(
- jsApi.ankiGetNewCardCount(),
- jsApi.ankiGetLrnCardCount(),
- jsApi.ankiGetRevCardCount()
+ getDataFromRequest("newCardCount", jsApi),
+ getDataFromRequest("lrnCardCount", jsApi),
+ getDataFromRequest("revCardCount", jsApi)
)
-
val expected = listOf(
- newCount,
- stepCount,
- revCount
+ formatApiResult(newCount),
+ formatApiResult(stepCount),
+ formatApiResult(revCount)
)
-
assertThat(
countList.toString(),
equalTo(expected.toString())
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt
index 660c567b6ab1..744e2fbb324e 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt
@@ -68,7 +68,7 @@ class AddonModelTest : RobolectricTest() {
assertEquals(addon.name, "valid-ankidroid-js-addon-test")
assertEquals(addon.addonTitle, "Valid AnkiDroid JS Addon")
assertEquals(addon.version, "1.0.0")
- assertEquals(addon.ankidroidJsApi, "0.0.1")
+ assertEquals(addon.ankidroidJsApi, "0.0.2")
assertEquals(addon.addonType, "reviewer")
assertEquals(addon.icon, "") // reviewer icon is empty
diff --git a/AnkiDroid/src/test/resources/test-js-addon.json b/AnkiDroid/src/test/resources/test-js-addon.json
index 271d3f56e10f..8aaa6376c777 100644
--- a/AnkiDroid/src/test/resources/test-js-addon.json
+++ b/AnkiDroid/src/test/resources/test-js-addon.json
@@ -5,7 +5,7 @@
"version": "1.1.1",
"description": "Show progress bar in AnkiDroid, this package may not be used in node_modules. For using this addon view. https://github.com/ankidroid/Anki-Android/pull/9232",
"main": "index.js",
- "ankidroidJsApi": "0.0.1",
+ "ankidroidJsApi": "0.0.2",
"addonType": "reviewer",
"keywords": [
"ankidroid-js-addon"
@@ -31,7 +31,7 @@
"version": "1.0.1",
"description": "This addon will listed in Addon Browser. Also AddonInfo.isValidAnkiDroidAddon return true for this package. For more view. https://github.com/ankidroid/Anki-Android/pull/9232",
"main": "index.js",
- "ankidroidJsApi": "0.0.1",
+ "ankidroidJsApi": "0.0.2",
"addonType": "reviewer",
"keywords": [
"ankidroid-js-addon"
diff --git a/AnkiDroid/src/test/resources/valid-ankidroid-js-addon-test.json b/AnkiDroid/src/test/resources/valid-ankidroid-js-addon-test.json
index 91505cc75e8b..b1b315d8dceb 100644
--- a/AnkiDroid/src/test/resources/valid-ankidroid-js-addon-test.json
+++ b/AnkiDroid/src/test/resources/valid-ankidroid-js-addon-test.json
@@ -2,7 +2,7 @@
"name": "valid-ankidroid-js-addon-test",
"addonTitle": "Valid AnkiDroid JS Addon",
"version": "1.0.0",
- "ankidroidJsApi": "0.0.1",
+ "ankidroidJsApi": "0.0.2",
"addonType": "reviewer",
"description": "This addon will listed in Addon Browser. Also AddonInfo.isValidAnkiDroidAddon return true for this package. For more view. https://github.com/ankidroid/Anki-Android/pull/9232",
"main": "index.js",