From 06e1a8807c681ed4b130b27e7e34597af42d71f6 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:07:41 +0800 Subject: [PATCH 01/10] convert synchronous js api to asynchronous and remove js interface check developer contract fo api requests which modify content - A class named AnkiDroidJS is used. When an object of this class is created, the init method must be called first. This method sets up the API with a developer contract. - For every API request that modifies content, the developer contract is checked. This is because each request creates a POST request, and the developer contract is passed in each of these requests. - Update js api, so it show error for previous version of deck - Make all request post, return value to bytearray, use array for api list - update tts api and other api to get data from request --- AnkiDroid/src/main/assets/card_template.html | 1 + AnkiDroid/src/main/assets/scripts/card.js | 47 --- AnkiDroid/src/main/assets/scripts/js-api.js | 136 +++++++ .../com/ichi2/anki/AbstractFlashcardViewer.kt | 55 +-- .../java/com/ichi2/anki/AnkiDroidJsAPI.kt | 351 +++++++++--------- .../com/ichi2/anki/AnkiDroidJsAPIConstants.kt | 4 +- .../src/main/java/com/ichi2/anki/Reviewer.kt | 126 +++++-- .../java/com/ichi2/anki/ReviewerServer.kt | 73 ++++ .../java/com/ichi2/anki/pages/AnkiServer.kt | 1 + .../java/com/ichi2/anki/AnkiDroidJsAPITest.kt | 253 +++++++------ .../test/java/com/ichi2/anki/ReviewerTest.kt | 12 +- .../com/ichi2/anki/jsaddons/AddonModelTest.kt | 2 +- .../src/test/resources/test-js-addon.json | 4 +- .../valid-ankidroid-js-addon-test.json | 2 +- 14 files changed, 627 insertions(+), 440 deletions(-) create mode 100644 AnkiDroid/src/main/assets/scripts/js-api.js 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..e92ff16c1853 --- /dev/null +++ b/AnkiDroid/src/main/assets/scripts/js-api.js @@ -0,0 +1,136 @@ +/* + * 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.init({ developer, version }); + } + + static init({ developer, version }) { + return new AnkiDroidJS({ developer, version }); + } + + async init({ developer, version }) { + this.developer = developer; + this.version = version; + return await this.handleRequest(`init`); + } + + handleRequest = async (endpoint, data) => { + if (!this.developer || !this.version) { + throw new Error("You must initialize API before using other JS API"); + } + + 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) { + if (this.version < "0.0.2") { + throw new Error("You must update AnkiDroid JS API version."); + } + const endpoint = jsApiList[method]; + const data = JSON.stringify({ text, queueMode }); + return await this.handleRequest(endpoint, data); + }; + return; + } + AnkiDroidJS.prototype[method] = async function (data) { + if (this.version < "0.0.2") { + throw new Error("You must update AnkiDroid JS API version."); + } + 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..3292a0f112c9 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 @@ -2295,58 +2294,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 +2502,7 @@ abstract class AbstractFlashcardViewer : } } - open fun javaScriptFunction(): AnkiDroidJsAPI? { + open fun javaScriptFunction(): AnkiDroidJsAPI { return AnkiDroidJsAPI(this) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index 25f7e81ceb23..4aa64c336130 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -22,7 +22,6 @@ 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 @@ -36,7 +35,8 @@ 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 @@ -54,6 +54,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { private val context: Context = activity private var cardSuppliedDeveloperContact = "" private var cardSuppliedApiVersion = "" + private var cardSuppliedData = "" // JS api list enable/disable status private var mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() @@ -61,13 +62,43 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { // Text to speech private val mTalker = JavaScriptTTS() + open fun convertToByteArray(boolean: Boolean): ByteArray { + return boolean.toString().toByteArray() + } + + open fun convertToByteArray(int: Int): ByteArray { + return int.toString().toByteArray() + } + + open fun convertToByteArray(long: Long): ByteArray { + return long.toString().toByteArray() + } + + open fun convertToByteArray(string: String): ByteArray { + return string.toByteArray() + } + // init or reset api list fun init() { cardSuppliedApiVersion = "" cardSuppliedDeveloperContact = "" + cardSuppliedData = "" mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() } + open fun checkJsApiContract(byteArray: ByteArray): String { + val data = JSONObject(byteArray.decodeToString()) + cardSuppliedApiVersion = data.optString("version", "") + cardSuppliedDeveloperContact = data.optString("developer", "") + cardSuppliedData = data.optString("data", "") + if (requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact)) { + enableJsApi() + } else { + mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() + } + return cardSuppliedData + } + // Check if value null private fun isAnkiApiNull(api: String): Boolean { return mJsApiListMap[api] == null @@ -81,11 +112,11 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * @param apiName * @param apiErrorCode */ - fun isInit(apiName: String, apiErrorCode: Int): Boolean { + private fun isInit(apiName: String, apiErrorCode: Int): Boolean { if (isAnkiApiNull(apiName)) { showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) return false - } else if (!getJsApiListMap()?.get(apiName)!!) { + } else if (!getJsApiListMap()[apiName]!!) { // see 02-string.xml showDeveloperContact(apiErrorCode) return false @@ -159,21 +190,14 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } } - protected fun getJsApiListMap(): HashMap? { + protected fun getJsApiListMap(): HashMap { return mJsApiListMap } - @JavascriptInterface - fun init(jsonData: String): String { - val data: JSONObject + suspend fun init(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { var apiStatusJson = "" try { - data = JSONObject(jsonData) - cardSuppliedApiVersion = data.optString("version", "") - cardSuppliedDeveloperContact = data.optString("developer", "") - if (requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact)) { - enableJsApi() - } + checkJsApiContract(byteArray) apiStatusJson = JSONObject(mJsApiListMap as Map).toString() } catch (j: JSONException) { Timber.w(j) @@ -181,296 +205,252 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { activity.showSnackbar(context.getString(R.string.invalid_json_data, j.localizedMessage)) } } - return apiStatusJson + convertToByteArray(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" + open suspend fun ankiGetNewCardCount(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(-1) } - @JavascriptInterface - open fun ankiGetLrnCardCount(): String? { - return "-1" + open suspend fun ankiGetLrnCardCount(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(-1) } - @JavascriptInterface - open fun ankiGetRevCardCount(): String? { - return "-1" + open suspend fun ankiGetRevCardCount(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(-1) } - @JavascriptInterface - open fun ankiGetETA(): Int { - return -1 + open suspend fun ankiGetETA(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(-1) } - @JavascriptInterface - fun ankiGetCardMark(): Boolean { - return currentCard.note().hasTag("marked") + suspend fun ankiGetCardMark(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.note().hasTag("marked")) } - @JavascriptInterface - fun ankiGetCardFlag(): Int { - return currentCard.userFlag() + suspend fun ankiGetCardFlag(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.userFlag()) } // behavior change ankiGetNextTime1...4 - @JavascriptInterface - open fun ankiGetNextTime1(): String { - return activity.easeButton1!!.nextTime + open suspend fun ankiGetNextTime1(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.easeButton1!!.nextTime) } - @JavascriptInterface - open fun ankiGetNextTime2(): String { - return activity.easeButton2!!.nextTime + open suspend fun ankiGetNextTime2(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.easeButton2!!.nextTime) } - @JavascriptInterface - open fun ankiGetNextTime3(): String { - return activity.easeButton3!!.nextTime + open suspend fun ankiGetNextTime3(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.easeButton3!!.nextTime) } - @JavascriptInterface - open fun ankiGetNextTime4(): String { - return activity.easeButton4!!.nextTime + open suspend fun ankiGetNextTime4(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.easeButton4!!.nextTime) } - @JavascriptInterface - fun ankiGetCardReps(): Int { - return currentCard.reps + suspend fun ankiGetCardReps(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.reps) } - @JavascriptInterface - fun ankiGetCardInterval(): Int { - return currentCard.ivl + suspend fun ankiGetCardInterval(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.ivl) } /** Returns the ease as an int (percentage * 10). Default: 2500 (250%). Minimum: 1300 (130%) */ - @JavascriptInterface - fun ankiGetCardFactor(): Int { - return currentCard.factor + suspend fun ankiGetCardFactor(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.factor) } /** Returns the last modified time as a Unix timestamp in seconds. Example: 1477384099 */ - @JavascriptInterface - fun ankiGetCardMod(): Long { - return currentCard.mod + suspend fun ankiGetCardMod(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.mod) } /** Returns the ID of the card. Example: 1477380543053 */ - @JavascriptInterface - fun ankiGetCardId(): Long { - return currentCard.id + suspend fun ankiGetCardId(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.id) } /** Returns the ID of the note which generated the card. Example: 1590418157630 */ - @JavascriptInterface - fun ankiGetCardNid(): Long { - return currentCard.nid + suspend fun ankiGetCardNid(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.nid) } - @JavascriptInterface @CARD_TYPE - fun ankiGetCardType(): Int { - return currentCard.type + suspend fun ankiGetCardType(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.type) } /** Returns the ID of the deck which contains the card. Example: 1595967594978 */ - @JavascriptInterface - fun ankiGetCardDid(): Long { - return currentCard.did + suspend fun ankiGetCardDid(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.did) } - @JavascriptInterface - fun ankiGetCardLeft(): Int { - return currentCard.left + suspend fun ankiGetCardLeft(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(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 + suspend fun ankiGetCardODid(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.oDid) } - @JavascriptInterface - fun ankiGetCardODue(): Long { - return currentCard.oDue + suspend fun ankiGetCardODue(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.oDue) } - @JavascriptInterface @CARD_QUEUE - fun ankiGetCardQueue(): Int { - return currentCard.queue + suspend fun ankiGetCardQueue(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.queue) } - @JavascriptInterface - fun ankiGetCardLapses(): Int { - return currentCard.lapses + suspend fun ankiGetCardLapses(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.lapses) } - @JavascriptInterface - fun ankiGetCardDue(): Long { - return currentCard.due + suspend fun ankiGetCardDue(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(currentCard.due) } - @JavascriptInterface - fun ankiIsInFullscreen(): Boolean { - return activity.isFullscreen + suspend fun ankiIsInFullscreen(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.isFullscreen) } - @JavascriptInterface - fun ankiIsTopbarShown(): Boolean { - return activity.prefShowTopbar + suspend fun ankiIsTopbarShown(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.prefShowTopbar) } - @JavascriptInterface - fun ankiIsInNightMode(): Boolean { - return activity.isInNightMode + suspend fun ankiIsInNightMode(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.isInNightMode) } - @JavascriptInterface - fun ankiIsDisplayingAnswer(): Boolean { - return activity.isDisplayingAnswer + suspend fun ankiIsDisplayingAnswer(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(activity.isDisplayingAnswer) } - @JavascriptInterface - fun ankiGetDeckName(): String { - return Decks.basename(activity.getColUnsafe.decks.name(currentCard.did)) + suspend fun ankiGetDeckName(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(Decks.basename(activity.getColUnsafe.decks.name(currentCard.did))) } - @JavascriptInterface - fun ankiBuryCard(): Boolean { + suspend fun ankiBuryCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) if (!isInit(AnkiDroidJsAPIConstants.BURY_CARD, AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryCard)) { - return false + return@withContext convertToByteArray(false) } - return activity.buryCard() + convertToByteArray(activity.buryCard()) } - @JavascriptInterface - fun ankiBuryNote(): Boolean { + suspend fun ankiBuryNote(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) if (!isInit(AnkiDroidJsAPIConstants.BURY_NOTE, AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryNote)) { - return false + return@withContext convertToByteArray(false) } - return activity.buryNote() + convertToByteArray(activity.buryNote()) } - @JavascriptInterface - fun ankiSuspendCard(): Boolean { + suspend fun ankiSuspendCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) if (!isInit(AnkiDroidJsAPIConstants.SUSPEND_CARD, AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendCard)) { - return false + return@withContext convertToByteArray(false) } - return activity.suspendCard() + convertToByteArray(activity.suspendCard()) } - @JavascriptInterface - fun ankiSuspendNote(): Boolean { + suspend fun ankiSuspendNote(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) if (!isInit(AnkiDroidJsAPIConstants.SUSPEND_NOTE, AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendNote)) { - return false + return@withContext convertToByteArray(false) } - return activity.suspendNote() + convertToByteArray(activity.suspendNote()) } - @JavascriptInterface - fun ankiAddTagToCard() { + suspend fun ankiAddTagToCard(): ByteArray = withContext(Dispatchers.Main) { activity.runOnUiThread { activity.showTagsDialog() } + convertToByteArray(true) } - @JavascriptInterface - fun ankiSearchCard(query: String?) { + suspend fun ankiSearchCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val query = checkJsApiContract(byteArray) 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) + convertToByteArray(true) } - @JavascriptInterface - fun ankiIsActiveNetworkMetered(): Boolean { - return NetworkUtils.isActiveNetworkMetered() + suspend fun ankiIsActiveNetworkMetered(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(NetworkUtils.isActiveNetworkMetered()) } // Know if {{tts}} is supported - issue #10443 // Return false for now - @JavascriptInterface - fun ankiTtsFieldModifierIsAvailable(): Boolean { - return false + suspend fun ankiTtsFieldModifierIsAvailable(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(false) } - @JavascriptInterface - fun ankiTtsSpeak(text: String?, queueMode: Int): Int { - return mTalker.speak(text, queueMode) + suspend fun ankiTtsSpeak(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + val jsonObject = JSONObject(data) + val text = jsonObject.getString("text") + val queueMode = jsonObject.getInt("queueMode") + convertToByteArray(mTalker.speak(text, queueMode)) } - @JavascriptInterface - fun ankiTtsSpeak(text: String?): Int { - return mTalker.speak(text) + suspend fun ankiTtsSetLanguage(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val loc = checkJsApiContract(byteArray) + convertToByteArray(mTalker.setLanguage(loc)) } - @JavascriptInterface - fun ankiTtsSetLanguage(loc: String): Int { - return mTalker.setLanguage(loc) + suspend fun ankiTtsSetPitch(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val pitch = checkJsApiContract(byteArray) + convertToByteArray(mTalker.setPitch(pitch.toFloat())) } - @JavascriptInterface - fun ankiTtsSetPitch(pitch: Float): Int { - return mTalker.setPitch(pitch) + suspend fun ankiTtsSetSpeechRate(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val speechRate = checkJsApiContract(byteArray) + convertToByteArray(mTalker.setSpeechRate(speechRate.toFloat())) } - @JavascriptInterface - fun ankiTtsSetPitch(pitch: Double): Int { - return mTalker.setPitch(pitch.toFloat()) + suspend fun ankiTtsIsSpeaking(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(mTalker.isSpeaking) } - @JavascriptInterface - fun ankiTtsSetSpeechRate(speechRate: Float): Int { - return mTalker.setSpeechRate(speechRate) + suspend fun ankiTtsStop(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(mTalker.stop()) } - @JavascriptInterface - fun ankiTtsSetSpeechRate(speechRate: Double): Int { - return mTalker.setSpeechRate(speechRate.toFloat()) + suspend fun ankiEnableHorizontalScrollbar(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val scroll = checkJsApiContract(byteArray) + activity.webView!!.isHorizontalScrollBarEnabled = scroll.toBoolean() + convertToByteArray(true) } - @JavascriptInterface - fun ankiTtsIsSpeaking(): Boolean { - return mTalker.isSpeaking + suspend fun ankiEnableVerticalScrollbar(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val scroll = checkJsApiContract(byteArray) + activity.webView!!.isVerticalScrollBarEnabled = scroll.toBoolean() + convertToByteArray(true) } - @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) { + suspend fun ankiSearchCardWithCallback(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val query = checkJsApiContract(byteArray) val cards = try { - runBlocking { - searchForCards(query, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) - } + searchForCards(query, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) } catch (exc: Exception) { activity.webView!!.evaluateJavascript( "console.log('${context.getString(R.string.search_card_js_api_no_results)}')", null ) - return + return@withContext convertToByteArray(false) } val searchResult: MutableList = ArrayList() for (s in cards) { @@ -493,23 +473,34 @@ 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(true) } - @JavascriptInterface - open fun ankiSetCardDue(days: Int): Boolean { + open suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { // the function is overridden in Reviewer.kt // it may be called in previewer so just return true value here - return true + convertToByteArray(true) } - @JavascriptInterface - open fun ankiResetProgress(): Boolean { + open suspend fun ankiResetProgress(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { // the function is overridden in Reviewer.kt // it may be called in previewer so just return true value here - return true + convertToByteArray(true) + } + + open suspend fun ankiMarkCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + // the function is overridden in Reviewer.kt + // it may be called in previewer so just return true value here + convertToByteArray(true) + } + + open suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + // the function is overridden in Reviewer.kt + // it may be called in previewer so just return true value here + convertToByteArray(true) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt index bf206890853a..138b09a761a3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt @@ -31,8 +31,8 @@ object AnkiDroidJsAPIConstants { const val ankiJsErrorCodeSetDue: Int = 7 // js api developer contact - const val sCurrentJsApiVersion = "0.0.1" - const val sMinimumJsApiVersion = "0.0.1" + const val sCurrentJsApiVersion = "0.0.2" + const val sMinimumJsApiVersion = "0.0.2" const val MARK_CARD = "markCard" const val TOGGLE_FLAG = "toggleFlag" diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 4cec789d12b3..17500b41df18 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,8 +46,10 @@ 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.MARK_CARD import com.ichi2.anki.AnkiDroidJsAPIConstants.RESET_PROGRESS import com.ichi2.anki.AnkiDroidJsAPIConstants.SET_CARD_DUE +import com.ichi2.anki.AnkiDroidJsAPIConstants.TOGGLE_FLAG import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeSetDue import com.ichi2.anki.CollectionManager.withCol @@ -90,6 +91,8 @@ import com.ichi2.utils.HandlerUtils.getDefaultLooper import com.ichi2.utils.Permissions.canRecordAudio import com.ichi2.utils.ViewGroupUtils.setRenderWorkaround import com.ichi2.widget.WidgetStatus.updateInBackground +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.util.function.Consumer @@ -1562,78 +1565,125 @@ open class Reviewer : } inner class ReviewerJavaScriptFunction(activity: AbstractFlashcardViewer) : AnkiDroidJsAPI(activity) { - @JavascriptInterface - override fun ankiGetNewCardCount(): String { - return mNewCount.toString() + override suspend fun ankiGetNewCardCount(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(mNewCount.toString()) } - @JavascriptInterface - override fun ankiGetLrnCardCount(): String { - return mLrnCount.toString() + override suspend fun ankiGetLrnCardCount(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(mLrnCount.toString()) } - @JavascriptInterface - override fun ankiGetRevCardCount(): String { - return mRevCount.toString() + override suspend fun ankiGetRevCardCount(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(mRevCount.toString()) } - @JavascriptInterface - override fun ankiGetETA(): Int { - return mEta + override suspend fun ankiGetETA(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(mEta) } - @JavascriptInterface - override fun ankiGetNextTime1(): String { - return easeButton1!!.nextTime + override suspend fun ankiGetNextTime1(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(easeButton1!!.nextTime) } - @JavascriptInterface - override fun ankiGetNextTime2(): String { - return easeButton2!!.nextTime + override suspend fun ankiGetNextTime2(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(easeButton2!!.nextTime) } - @JavascriptInterface - override fun ankiGetNextTime3(): String { - return easeButton3!!.nextTime + override suspend fun ankiGetNextTime3(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(easeButton3!!.nextTime) } - @JavascriptInterface - override fun ankiGetNextTime4(): String { - return easeButton4!!.nextTime + override suspend fun ankiGetNextTime4(): ByteArray = withContext(Dispatchers.Main) { + convertToByteArray(easeButton4!!.nextTime) } - @JavascriptInterface - override fun ankiSetCardDue(days: Int): Boolean { - val apiList = getJsApiListMap()!! + override suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val days = checkJsApiContract(byteArray) + val daysInt = days.toInt() + val apiList = getJsApiListMap() if (!apiList[SET_CARD_DUE]!!) { showDeveloperContact(ankiJsErrorCodeDefault) - return false + return@withContext convertToByteArray(false) } - if (days < 0 || days > 9999) { + if (daysInt < 0 || daysInt > 9999) { showDeveloperContact(ankiJsErrorCodeSetDue) - return false + convertToByteArray(false) } val cardIds = listOf(currentCard!!.id) launchCatchingTask { - rescheduleCards(cardIds, days) + rescheduleCards(cardIds, daysInt) } - return true + convertToByteArray(true) } - @JavascriptInterface - override fun ankiResetProgress(): Boolean { - val apiList = getJsApiListMap()!! + override suspend fun ankiResetProgress(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) + val apiList = getJsApiListMap() if (!apiList[RESET_PROGRESS]!!) { showDeveloperContact(ankiJsErrorCodeDefault) - return false + return@withContext convertToByteArray(false) } val cardIds = listOf(currentCard!!.id) launchCatchingTask { resetCards(cardIds) } - return true + convertToByteArray(true) + } + + override suspend fun ankiMarkCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) + val apiList = getJsApiListMap() + if (!apiList[MARK_CARD]!!) { + showDeveloperContact(ankiJsErrorCodeDefault) + return@withContext convertToByteArray(false) + } + + executeCommand(ViewerCommand.MARK) + convertToByteArray(true) + } + + override suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val flag = checkJsApiContract(byteArray) + // flag card (blue, green, orange, red) using javascript from AnkiDroid webview + val apiList = getJsApiListMap() + if (!apiList[TOGGLE_FLAG]!!) { + showDeveloperContact(ankiJsErrorCodeDefault) + return@withContext convertToByteArray(false) + } + + when (flag) { + "none" -> { + executeCommand(ViewerCommand.UNSET_FLAG) + } + "red" -> { + executeCommand(ViewerCommand.TOGGLE_FLAG_RED) + } + "orange" -> { + executeCommand(ViewerCommand.TOGGLE_FLAG_ORANGE) + } + "green" -> { + executeCommand(ViewerCommand.TOGGLE_FLAG_GREEN) + } + "blue" -> { + executeCommand(ViewerCommand.TOGGLE_FLAG_BLUE) + } + "pink" -> { + executeCommand(ViewerCommand.TOGGLE_FLAG_PINK) + } + "turquoise" -> { + executeCommand(ViewerCommand.TOGGLE_FLAG_TURQUOISE) + } + "purple" -> { + executeCommand(ViewerCommand.TOGGLE_FLAG_PURPLE) + } + else -> { + Timber.d("No such Flag found.") + convertToByteArray(false) + } + } + convertToByteArray(true) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt index 9e4972bfc4dd..3eb05a12851e 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 = "" + 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 { + handleJsApiPostRequest(uri.substring(ANKIDROID_JS_PREFIX.length), inputBytes) + } + } } Timber.w("not found: $uri") @@ -88,10 +98,73 @@ class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiSer } } + private suspend fun handleJsApiPostRequest(methodName: String, bytes: ByteArray): ByteArray { + return when (methodName) { + "init" -> jsApi.init(bytes) + "newCardCount" -> jsApi.ankiGetNewCardCount() + "lrnCardCount" -> jsApi.ankiGetLrnCardCount() + "revCardCount" -> jsApi.ankiGetRevCardCount() + "eta" -> jsApi.ankiGetETA() + "cardMark" -> jsApi.ankiGetCardMark() + "cardFlag" -> jsApi.ankiGetCardFlag() + "cardReps" -> jsApi.ankiGetCardReps() + "cardInterval" -> jsApi.ankiGetCardInterval() + "cardFactor" -> jsApi.ankiGetCardFactor() + "cardMod" -> jsApi.ankiGetCardMod() + "cardId" -> jsApi.ankiGetCardId() + "cardNid" -> jsApi.ankiGetCardNid() + "cardType" -> jsApi.ankiGetCardType() + "cardDid" -> jsApi.ankiGetCardDid() + "cardLeft" -> jsApi.ankiGetCardLeft() + "cardODid" -> jsApi.ankiGetCardODid() + "cardODue" -> jsApi.ankiGetCardODue() + "cardQueue" -> jsApi.ankiGetCardQueue() + "cardLapses" -> jsApi.ankiGetCardLapses() + "cardDue" -> jsApi.ankiGetCardDue() + "deckName" -> jsApi.ankiGetDeckName() + "isActiveNetworkMetered" -> jsApi.ankiIsActiveNetworkMetered() + "ttsSetLanguage" -> jsApi.ankiTtsSetLanguage(bytes) + "ttsSpeak" -> jsApi.ankiTtsSpeak(bytes) + "ttsIsSpeaking" -> jsApi.ankiTtsIsSpeaking() + "ttsSetPitch" -> jsApi.ankiTtsSetPitch(bytes) + "ttsSetSpeechRate" -> jsApi.ankiTtsSetSpeechRate(bytes) + "ttsFieldModifierIsAvailable" -> jsApi.ankiTtsFieldModifierIsAvailable() + "ttsStop" -> jsApi.ankiTtsStop() + "nextTime1" -> jsApi.ankiGetNextTime1() + "nextTime2" -> jsApi.ankiGetNextTime2() + "nextTime3" -> jsApi.ankiGetNextTime3() + "nextTime4" -> jsApi.ankiGetNextTime4() + "searchCard" -> jsApi.ankiSearchCard(bytes) + "searchCardWithCallback" -> jsApi.ankiSearchCardWithCallback(bytes) + "buryCard" -> jsApi.ankiBuryCard(bytes) + "buryNote" -> jsApi.ankiBuryNote(bytes) + "suspendCard" -> jsApi.ankiSuspendCard(bytes) + "suspendNote" -> jsApi.ankiSuspendNote(bytes) + "setCardDue" -> jsApi.ankiSetCardDue(bytes) + "resetProgress" -> jsApi.ankiResetProgress(bytes) + "isDisplayingAnswer" -> jsApi.ankiIsDisplayingAnswer() + "addTagToCard" -> jsApi.ankiAddTagToCard() + "isInFullscreen" -> jsApi.ankiIsInFullscreen() + "isTopbarShown" -> jsApi.ankiIsTopbarShown() + "isInNightMode" -> jsApi.ankiIsInNightMode() + "enableHorizontalScrollbar" -> jsApi.ankiEnableHorizontalScrollbar(bytes) + "enableVerticalScrollbar" -> jsApi.ankiEnableVerticalScrollbar(bytes) + "toggleFlag" -> jsApi.ankiToggleFlag(bytes) + "markCard" -> jsApi.ankiMarkCard(bytes) + else -> { + throw Exception("unhandled request: $methodName") + } + } + } + private fun reviewer(): Reviewer { 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..1f66f3059916 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt @@ -34,7 +34,7 @@ import org.junit.runner.RunWith class AnkiDroidJsAPITest : RobolectricTest() { @Test - fun initTest() { + fun initTest() = runTest { val models = col.notetypes val decks = col.decks val didA = addDeck("Test") @@ -46,20 +46,17 @@ class AnkiDroidJsAPITest : RobolectricTest() { 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}" + val expected = + "{\"setCardDue\":true,\"suspendNote\":true,\"markCard\":true,\"suspendCard\":true,\"buryCard\":true,\"toggleFlag\":true,\"buryNote\":true}" waitForAsyncTasksToComplete() - assertThat(javaScriptFunction.init(data.toString()), equalTo(expected)) + assertThat(javaScriptFunction.init(jsApiContract()).decodeToString(), equalTo(expected)) } @Test - fun ankiGetNextTimeTest() { + fun ankiGetNextTimeTest() = runTest { val models = col.notetypes val decks = col.decks val didA = addDeck("Test") @@ -75,14 +72,26 @@ class AnkiDroidJsAPITest : RobolectricTest() { 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( + javaScriptFunction.ankiGetNextTime1().decodeToString().withoutUnicodeIsolation(), + equalTo("<1m") + ) + assertThat( + javaScriptFunction.ankiGetNextTime2().decodeToString().withoutUnicodeIsolation(), + equalTo("<6m") + ) + assertThat( + javaScriptFunction.ankiGetNextTime3().decodeToString().withoutUnicodeIsolation(), + equalTo("<10m") + ) + assertThat( + javaScriptFunction.ankiGetNextTime4().decodeToString().withoutUnicodeIsolation(), + equalTo("4d") + ) } @Test - fun ankiTestCurrentCard() { + fun ankiTestCurrentCard() = runTest { val models = col.notetypes val decks = col.decks val didA = addDeck("Test") @@ -100,47 +109,92 @@ class AnkiDroidJsAPITest : RobolectricTest() { val currentCard = reviewer.currentCard!! // Card Did - assertThat(javaScriptFunction.ankiGetCardDid(), equalTo(currentCard.did)) + assertThat( + javaScriptFunction.ankiGetCardDid().decodeToString().toLong(), + equalTo(currentCard.did) + ) // Card Id - assertThat(javaScriptFunction.ankiGetCardId(), equalTo(currentCard.id)) + assertThat( + javaScriptFunction.ankiGetCardId().decodeToString().toLong(), + equalTo(currentCard.id) + ) // Card Nid - assertThat(javaScriptFunction.ankiGetCardNid(), equalTo(currentCard.nid)) + assertThat( + javaScriptFunction.ankiGetCardNid().decodeToString().toLong(), + equalTo(currentCard.nid) + ) // Card ODid - assertThat(javaScriptFunction.ankiGetCardODid(), equalTo(currentCard.oDid)) + assertThat( + javaScriptFunction.ankiGetCardODid().decodeToString().toLong(), + equalTo(currentCard.oDid) + ) // Card Type - assertThat(javaScriptFunction.ankiGetCardType(), equalTo(currentCard.type)) + assertThat( + javaScriptFunction.ankiGetCardType().decodeToString().toInt(), + equalTo(currentCard.type) + ) // Card ODue - assertThat(javaScriptFunction.ankiGetCardODue(), equalTo(currentCard.oDue)) + assertThat( + javaScriptFunction.ankiGetCardODue().decodeToString().toLong(), + equalTo(currentCard.oDue) + ) // Card Due - assertThat(javaScriptFunction.ankiGetCardDue(), equalTo(currentCard.due)) + assertThat( + javaScriptFunction.ankiGetCardDue().decodeToString().toLong(), + equalTo(currentCard.due) + ) // Card Factor - assertThat(javaScriptFunction.ankiGetCardFactor(), equalTo(currentCard.factor)) + assertThat( + javaScriptFunction.ankiGetCardFactor().decodeToString().toInt(), + equalTo(currentCard.factor) + ) // Card Lapses - assertThat(javaScriptFunction.ankiGetCardLapses(), equalTo(currentCard.lapses)) + assertThat( + javaScriptFunction.ankiGetCardLapses().decodeToString().toInt(), + equalTo(currentCard.lapses) + ) // Card Ivl - assertThat(javaScriptFunction.ankiGetCardInterval(), equalTo(currentCard.ivl)) + assertThat( + javaScriptFunction.ankiGetCardInterval().decodeToString().toInt(), + equalTo(currentCard.ivl) + ) // Card mod - assertThat(javaScriptFunction.ankiGetCardMod(), equalTo(currentCard.mod)) + assertThat( + javaScriptFunction.ankiGetCardMod().decodeToString().toLong(), + equalTo(currentCard.mod) + ) // Card Queue - assertThat(javaScriptFunction.ankiGetCardQueue(), equalTo(currentCard.queue)) + assertThat( + javaScriptFunction.ankiGetCardQueue().decodeToString().toInt(), + equalTo(currentCard.queue) + ) // Card Reps - assertThat(javaScriptFunction.ankiGetCardReps(), equalTo(currentCard.reps)) + assertThat( + javaScriptFunction.ankiGetCardReps().decodeToString().toInt(), + equalTo(currentCard.reps) + ) // Card left - assertThat(javaScriptFunction.ankiGetCardLeft(), equalTo(currentCard.left)) + assertThat( + javaScriptFunction.ankiGetCardLeft().decodeToString().toInt(), + equalTo(currentCard.left) + ) // Card Flag - assertThat(javaScriptFunction.ankiGetCardFlag(), equalTo(0)) + assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), equalTo(0)) reviewer.currentCard!!.setFlag(1) - assertThat(javaScriptFunction.ankiGetCardFlag(), equalTo(1)) + assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), equalTo(1)) // Card Mark - assertThat(javaScriptFunction.ankiGetCardMark(), equalTo(false)) + assertThat( + javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), + equalTo(false) + ) reviewer.currentCard!!.note().addTag("marked") - assertThat(javaScriptFunction.ankiGetCardMark(), equalTo(true)) + assertThat(javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), equalTo(true)) } @Test - fun ankiJsUiTest() { + fun ankiJsUiTest() = runTest { val models = col.notetypes val decks = col.decks val didA = addDeck("Test") @@ -155,20 +209,35 @@ class AnkiDroidJsAPITest : RobolectricTest() { waitForAsyncTasksToComplete() // Displaying question - assertThat(javaScriptFunction.ankiIsDisplayingAnswer(), equalTo(reviewer.isDisplayingAnswer)) + assertThat( + javaScriptFunction.ankiIsDisplayingAnswer().decodeToString().toBoolean(), + equalTo(reviewer.isDisplayingAnswer) + ) reviewer.displayCardAnswer() - assertThat(javaScriptFunction.ankiIsDisplayingAnswer(), equalTo(reviewer.isDisplayingAnswer)) + assertThat( + javaScriptFunction.ankiIsDisplayingAnswer().decodeToString().toBoolean(), + equalTo(reviewer.isDisplayingAnswer) + ) // Full Screen - assertThat(javaScriptFunction.ankiIsInFullscreen(), equalTo(reviewer.isFullscreen)) + assertThat( + javaScriptFunction.ankiIsInFullscreen().decodeToString().toBoolean(), + equalTo(reviewer.isFullscreen) + ) // Top bar - assertThat(javaScriptFunction.ankiIsTopbarShown(), equalTo(reviewer.prefShowTopbar)) + assertThat( + javaScriptFunction.ankiIsTopbarShown().decodeToString().toBoolean(), + equalTo(reviewer.prefShowTopbar) + ) // Night Mode - assertThat(javaScriptFunction.ankiIsInNightMode(), equalTo(reviewer.isInNightMode)) + assertThat( + javaScriptFunction.ankiIsInNightMode().decodeToString().toBoolean(), + equalTo(reviewer.isInNightMode) + ) } @Test - fun ankiMarkAndFlagCardTest() { + fun ankiMarkAndFlagCardTest() = runTest { // js api test for marking and flagging card val models = col.notetypes val decks = col.decks @@ -187,52 +256,28 @@ 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" + assertThat( + javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), + equalTo(false) + ) // get card mark status for test - markCardJs += "AnkiDroidJS.ankiGetCardMark();\n" + - "})();" - - reviewer.webView!!.evaluateJavascript(markCardJs) { s -> assertThat(s, equalTo(true)) } + javaScriptFunction.ankiMarkCard(jsApiContract()) + assertThat(javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), equalTo(true)) // --------------- // Card flag test // --------------- // before toggling flag - assertThat(javaScriptFunction.ankiGetCardFlag(), equalTo(0)) + assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), 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)) } + javaScriptFunction.ankiToggleFlag(jsApiContract("red")) + assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), equalTo(1)) } - fun ankiBurySuspendTest() { + // TODO - update test + 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 +295,17 @@ class AnkiDroidJsAPITest : RobolectricTest() { decks.select(didA) val reviewer: Reviewer = startReviewer() - + val javaScriptFunction = reviewer.javaScriptFunction() + // init js api + javaScriptFunction.init(jsApiContract()) waitForAsyncTasksToComplete() // ---------- // Bury Card // ---------- - var jsScript = createTestScript("AnkiDroidJS.ankiBuryCard();") // call script to bury current card - reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) } + javaScriptFunction.ankiBuryCard(jsApiContract()) + waitForAsyncTasksToComplete() // count number of notes val sched = reviewer.getColUnsafe @@ -267,9 +314,8 @@ class AnkiDroidJsAPITest : RobolectricTest() { // ---------- // Bury Note // ---------- - jsScript = createTestScript("AnkiDroidJS.ankiBuryNote();") // call script to bury current note - reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) } + javaScriptFunction.ankiBuryNote(jsApiContract()) // count number of notes assertThat(sched.cardCount(), equalTo(3)) @@ -277,9 +323,8 @@ class AnkiDroidJsAPITest : RobolectricTest() { // ------------- // Suspend Card // ------------- - jsScript = createTestScript("AnkiDroidJS.ankiSuspendCard();") // call script to suspend current card - reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) } + javaScriptFunction.ankiSuspendCard(jsApiContract()) // count number of notes assertThat(sched.cardCount(), equalTo(2)) @@ -287,30 +332,13 @@ class AnkiDroidJsAPITest : RobolectricTest() { // ------------- // Suspend Note // ------------- - jsScript = createTestScript("AnkiDroidJS.ankiSuspendNote();") // call script to suspend current note - reviewer.webView!!.evaluateJavascript(jsScript) { s -> assertThat(s, equalTo(true)) } + javaScriptFunction.ankiSuspendNote(jsApiContract()) // 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) } @@ -332,12 +360,15 @@ class AnkiDroidJsAPITest : RobolectricTest() { val javaScriptFunction = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(initJsApiContract()) + javaScriptFunction.init(jsApiContract()) // get card id for testing due - val cardId = javaScriptFunction.ankiGetCardId() + val cardId = javaScriptFunction.ankiGetCardId().decodeToString().toLong() // test that card rescheduled for 15 days interval and returned true - assertTrue("Card rescheduled, so returns true", javaScriptFunction.ankiSetCardDue(15)) + assertTrue( + "Card rescheduled, so returns true", + javaScriptFunction.ankiSetCardDue(jsApiContract("15")).decodeToString().toBoolean() + ) waitForAsyncTasksToComplete() // verify that it did get rescheduled @@ -346,11 +377,12 @@ class AnkiDroidJsAPITest : RobolectricTest() { 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() + private fun jsApiContract(data: String = ""): ByteArray { + val jsonObject = JSONObject() + jsonObject.put("version", "0.0.2") + jsonObject.put("developer", "test@example.com") + jsonObject.put("data", data) + return jsonObject.toString().toByteArray() } @Test @@ -375,12 +407,15 @@ class AnkiDroidJsAPITest : RobolectricTest() { val javaScriptFunction = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(initJsApiContract()) + javaScriptFunction.init(jsApiContract()) // get card id for testing due - val cardId = javaScriptFunction.ankiGetCardId() + val cardId = javaScriptFunction.ankiGetCardId().decodeToString().toLong() // test that card reset - assertTrue("Card progress reset", javaScriptFunction.ankiResetProgress()) + assertTrue( + "Card progress reset", + javaScriptFunction.ankiResetProgress(jsApiContract()).decodeToString().toBoolean() + ) waitForAsyncTasksToComplete() // verify that card progress reset diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt index e468b678e33a..c26d0eb6322a 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt @@ -222,7 +222,7 @@ class ReviewerTest : RobolectricTest() { } @Test - fun jsAnkiGetDeckName() { + fun jsAnkiGetDeckName() = runTest { val models = col.notetypes val decks = col.decks @@ -238,7 +238,7 @@ class ReviewerTest : RobolectricTest() { val javaScriptFunction = reviewer.javaScriptFunction() waitForAsyncTasksToComplete() - assertThat(javaScriptFunction.ankiGetDeckName(), equalTo("B")) + assertThat(javaScriptFunction.ankiGetDeckName().decodeToString(), equalTo("B")) } @Ignore("needs update for v3") @@ -324,12 +324,12 @@ 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() + jsApi.ankiGetNewCardCount().decodeToString().toInt(), + jsApi.ankiGetLrnCardCount().decodeToString().toInt(), + jsApi.ankiGetRevCardCount().decodeToString().toInt() ) val expected = listOf( 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", From daa9a00ea5fb56ce5bfa6b1a5f7f68d9ff6481ce Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Sat, 2 Dec 2023 00:51:51 +0800 Subject: [PATCH 02/10] add docs to method --- AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index 4aa64c336130..b4bb6ae1695d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -86,6 +86,14 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() } + /** + * The method parse json data and check for api version, developer contact + * and extract card supplied data if api version and developer contact + * provided then enable js api otherwise disable js api. + * @param byteArray + * @return card supplied data, it may be empty, or specific to js api, + * in case of tts api it contains json string of text and queueMode which parsed in speak tts api + */ open fun checkJsApiContract(byteArray: ByteArray): String { val data = JSONObject(byteArray.decodeToString()) cardSuppliedApiVersion = data.optString("version", "") From 9195c17f7bac46b0e4275cc73a8062fbfb488a42 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:03:41 +0800 Subject: [PATCH 03/10] check contract for each api request and remove redudant init method --- AnkiDroid/src/main/assets/scripts/js-api.js | 6 +- .../java/com/ichi2/anki/AnkiDroidJsAPI.kt | 251 ++++++++++++++---- .../src/main/java/com/ichi2/anki/Reviewer.kt | 54 +++- .../java/com/ichi2/anki/ReviewerServer.kt | 68 ++--- 4 files changed, 277 insertions(+), 102 deletions(-) diff --git a/AnkiDroid/src/main/assets/scripts/js-api.js b/AnkiDroid/src/main/assets/scripts/js-api.js index e92ff16c1853..b33d3e9ad0be 100644 --- a/AnkiDroid/src/main/assets/scripts/js-api.js +++ b/AnkiDroid/src/main/assets/scripts/js-api.js @@ -66,16 +66,14 @@ class AnkiDroidJS { constructor({ developer, version }) { this.developer = developer; this.version = version; - this.init({ developer, version }); + this.initJSRequest(); } static init({ developer, version }) { return new AnkiDroidJS({ developer, version }); } - async init({ developer, version }) { - this.developer = developer; - this.version = version; + async initJSRequest() { return await this.handleRequest(`init`); } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index b4bb6ae1695d..3cc7ff8d8aad 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -48,7 +48,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { /** 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 @@ -94,17 +94,18 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * @return card supplied data, it may be empty, or specific to js api, * in case of tts api it contains json string of text and queueMode which parsed in speak tts api */ - open fun checkJsApiContract(byteArray: ByteArray): String { + open fun checkJsApiContract(byteArray: ByteArray): Pair { val data = JSONObject(byteArray.decodeToString()) cardSuppliedApiVersion = data.optString("version", "") cardSuppliedDeveloperContact = data.optString("developer", "") cardSuppliedData = data.optString("data", "") - if (requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact)) { + val isValidVersion = requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact) + if (isValidVersion) { enableJsApi() } else { mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() } - return cardSuppliedData + return Pair(isValidVersion, cardSuppliedData) } // Check if value null @@ -220,128 +221,228 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { // 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. - open suspend fun ankiGetNewCardCount(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetNewCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { convertToByteArray(-1) } - open suspend fun ankiGetLrnCardCount(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetLrnCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { convertToByteArray(-1) } - open suspend fun ankiGetRevCardCount(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetRevCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { convertToByteArray(-1) } - open suspend fun ankiGetETA(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetETA(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { convertToByteArray(-1) } - suspend fun ankiGetCardMark(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardMark(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.note().hasTag("marked")) } - suspend fun ankiGetCardFlag(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.userFlag()) } // behavior change ankiGetNextTime1...4 - open suspend fun ankiGetNextTime1(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetNextTime1(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.easeButton1!!.nextTime) } - open suspend fun ankiGetNextTime2(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetNextTime2(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.easeButton2!!.nextTime) } - open suspend fun ankiGetNextTime3(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetNextTime3(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.easeButton3!!.nextTime) } - open suspend fun ankiGetNextTime4(): ByteArray = withContext(Dispatchers.Main) { + open suspend fun ankiGetNextTime4(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.easeButton4!!.nextTime) } - suspend fun ankiGetCardReps(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardReps(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.reps) } - suspend fun ankiGetCardInterval(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardInterval(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.ivl) } /** Returns the ease as an int (percentage * 10). Default: 2500 (250%). Minimum: 1300 (130%) */ - suspend fun ankiGetCardFactor(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardFactor(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.factor) } /** Returns the last modified time as a Unix timestamp in seconds. Example: 1477384099 */ - suspend fun ankiGetCardMod(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardMod(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.mod) } /** Returns the ID of the card. Example: 1477380543053 */ - suspend fun ankiGetCardId(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardId(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.id) } /** Returns the ID of the note which generated the card. Example: 1590418157630 */ - suspend fun ankiGetCardNid(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardNid(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.nid) } @CARD_TYPE - suspend fun ankiGetCardType(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardType(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.type) } /** Returns the ID of the deck which contains the card. Example: 1595967594978 */ - suspend fun ankiGetCardDid(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardDid(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.did) } - suspend fun ankiGetCardLeft(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardLeft(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.left) } /** Returns the ID of the home deck for the card if it is filtered, or 0 if not filtered. Example: 1595967594978 */ - suspend fun ankiGetCardODid(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardODid(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.oDid) } - suspend fun ankiGetCardODue(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardODue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.oDue) } @CARD_QUEUE - suspend fun ankiGetCardQueue(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardQueue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.queue) } - suspend fun ankiGetCardLapses(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardLapses(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.lapses) } - suspend fun ankiGetCardDue(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(currentCard.due) } - suspend fun ankiIsInFullscreen(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiIsInFullscreen(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.isFullscreen) } - suspend fun ankiIsTopbarShown(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiIsTopbarShown(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.prefShowTopbar) } - suspend fun ankiIsInNightMode(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiIsInNightMode(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.isInNightMode) } - suspend fun ankiIsDisplayingAnswer(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiIsDisplayingAnswer(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(activity.isDisplayingAnswer) } - suspend fun ankiGetDeckName(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiGetDeckName(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(Decks.basename(activity.getColUnsafe.decks.name(currentCard.did))) } @@ -381,78 +482,122 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { convertToByteArray(activity.suspendNote()) } - suspend fun ankiAddTagToCard(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiAddTagToCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } activity.runOnUiThread { activity.showTagsDialog() } convertToByteArray(true) } suspend fun ankiSearchCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val query = checkJsApiContract(byteArray) + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } val intent = Intent(context, CardBrowser::class.java) val currentCardId: CardId = currentCard.id intent.putExtra("currentCard", currentCardId) - intent.putExtra("search_query", query) + intent.putExtra("search_query", data.second) activity.startActivityWithAnimation(intent, ActivityTransitionAnimation.Direction.START) convertToByteArray(true) } - suspend fun ankiIsActiveNetworkMetered(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiIsActiveNetworkMetered(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(NetworkUtils.isActiveNetworkMetered()) } // Know if {{tts}} is supported - issue #10443 // Return false for now - suspend fun ankiTtsFieldModifierIsAvailable(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiTtsFieldModifierIsAvailable(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(false) } suspend fun ankiTtsSpeak(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { val data = checkJsApiContract(byteArray) - val jsonObject = JSONObject(data) + if (!data.first) { + return@withContext convertToByteArray(-1) + } + val jsonObject = JSONObject(data.second) val text = jsonObject.getString("text") val queueMode = jsonObject.getInt("queueMode") convertToByteArray(mTalker.speak(text, queueMode)) } suspend fun ankiTtsSetLanguage(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val loc = checkJsApiContract(byteArray) - convertToByteArray(mTalker.setLanguage(loc)) + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } + convertToByteArray(mTalker.setLanguage(data.second)) } suspend fun ankiTtsSetPitch(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val pitch = checkJsApiContract(byteArray) - convertToByteArray(mTalker.setPitch(pitch.toFloat())) + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } + convertToByteArray(mTalker.setPitch(data.second.toFloat())) } suspend fun ankiTtsSetSpeechRate(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val speechRate = checkJsApiContract(byteArray) - convertToByteArray(mTalker.setSpeechRate(speechRate.toFloat())) + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } + convertToByteArray(mTalker.setSpeechRate(data.second.toFloat())) } - suspend fun ankiTtsIsSpeaking(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiTtsIsSpeaking(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(mTalker.isSpeaking) } - suspend fun ankiTtsStop(): ByteArray = withContext(Dispatchers.Main) { + suspend fun ankiTtsStop(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } convertToByteArray(mTalker.stop()) } suspend fun ankiEnableHorizontalScrollbar(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val scroll = checkJsApiContract(byteArray) - activity.webView!!.isHorizontalScrollBarEnabled = scroll.toBoolean() + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } + activity.webView!!.isHorizontalScrollBarEnabled = data.second.toBoolean() convertToByteArray(true) } suspend fun ankiEnableVerticalScrollbar(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val scroll = checkJsApiContract(byteArray) - activity.webView!!.isVerticalScrollBarEnabled = scroll.toBoolean() + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } + activity.webView!!.isVerticalScrollBarEnabled = data.second.toBoolean() convertToByteArray(true) } suspend fun ankiSearchCardWithCallback(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val query = checkJsApiContract(byteArray) + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(false) + } val cards = try { - searchForCards(query, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) + searchForCards(data.second, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) } catch (exc: Exception) { activity.webView!!.evaluateJavascript( "console.log('${context.getString(R.string.search_card_js_api_no_results)}')", diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 17500b41df18..5fbf7933a449 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -1565,41 +1565,73 @@ open class Reviewer : } inner class ReviewerJavaScriptFunction(activity: AbstractFlashcardViewer) : AnkiDroidJsAPI(activity) { - override suspend fun ankiGetNewCardCount(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetNewCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(mNewCount.toString()) } - override suspend fun ankiGetLrnCardCount(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetLrnCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(mLrnCount.toString()) } - override suspend fun ankiGetRevCardCount(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetRevCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(mRevCount.toString()) } - override suspend fun ankiGetETA(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetETA(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(mEta) } - override suspend fun ankiGetNextTime1(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetNextTime1(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(easeButton1!!.nextTime) } - override suspend fun ankiGetNextTime2(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetNextTime2(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(easeButton2!!.nextTime) } - override suspend fun ankiGetNextTime3(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetNextTime3(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(easeButton3!!.nextTime) } - override suspend fun ankiGetNextTime4(): ByteArray = withContext(Dispatchers.Main) { + override suspend fun ankiGetNextTime4(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + if (!data.first) { + return@withContext convertToByteArray(-1) + } convertToByteArray(easeButton4!!.nextTime) } override suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val days = checkJsApiContract(byteArray) - val daysInt = days.toInt() + val data = checkJsApiContract(byteArray) + val daysInt = data.second.toInt() val apiList = getJsApiListMap() if (!apiList[SET_CARD_DUE]!!) { showDeveloperContact(ankiJsErrorCodeDefault) @@ -1645,7 +1677,7 @@ open class Reviewer : } override suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val flag = checkJsApiContract(byteArray) + val flag = checkJsApiContract(byteArray).second // flag card (blue, green, orange, red) using javascript from AnkiDroid webview val apiList = getJsApiListMap() if (!apiList[TOGGLE_FLAG]!!) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt index 3eb05a12851e..c5c15ee535ab 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt @@ -101,39 +101,39 @@ class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiSer private suspend fun handleJsApiPostRequest(methodName: String, bytes: ByteArray): ByteArray { return when (methodName) { "init" -> jsApi.init(bytes) - "newCardCount" -> jsApi.ankiGetNewCardCount() - "lrnCardCount" -> jsApi.ankiGetLrnCardCount() - "revCardCount" -> jsApi.ankiGetRevCardCount() - "eta" -> jsApi.ankiGetETA() - "cardMark" -> jsApi.ankiGetCardMark() - "cardFlag" -> jsApi.ankiGetCardFlag() - "cardReps" -> jsApi.ankiGetCardReps() - "cardInterval" -> jsApi.ankiGetCardInterval() - "cardFactor" -> jsApi.ankiGetCardFactor() - "cardMod" -> jsApi.ankiGetCardMod() - "cardId" -> jsApi.ankiGetCardId() - "cardNid" -> jsApi.ankiGetCardNid() - "cardType" -> jsApi.ankiGetCardType() - "cardDid" -> jsApi.ankiGetCardDid() - "cardLeft" -> jsApi.ankiGetCardLeft() - "cardODid" -> jsApi.ankiGetCardODid() - "cardODue" -> jsApi.ankiGetCardODue() - "cardQueue" -> jsApi.ankiGetCardQueue() - "cardLapses" -> jsApi.ankiGetCardLapses() - "cardDue" -> jsApi.ankiGetCardDue() - "deckName" -> jsApi.ankiGetDeckName() - "isActiveNetworkMetered" -> jsApi.ankiIsActiveNetworkMetered() + "newCardCount" -> jsApi.ankiGetNewCardCount(bytes) + "lrnCardCount" -> jsApi.ankiGetLrnCardCount(bytes) + "revCardCount" -> jsApi.ankiGetRevCardCount(bytes) + "eta" -> jsApi.ankiGetETA(bytes) + "cardMark" -> jsApi.ankiGetCardMark(bytes) + "cardFlag" -> jsApi.ankiGetCardFlag(bytes) + "cardReps" -> jsApi.ankiGetCardReps(bytes) + "cardInterval" -> jsApi.ankiGetCardInterval(bytes) + "cardFactor" -> jsApi.ankiGetCardFactor(bytes) + "cardMod" -> jsApi.ankiGetCardMod(bytes) + "cardId" -> jsApi.ankiGetCardId(bytes) + "cardNid" -> jsApi.ankiGetCardNid(bytes) + "cardType" -> jsApi.ankiGetCardType(bytes) + "cardDid" -> jsApi.ankiGetCardDid(bytes) + "cardLeft" -> jsApi.ankiGetCardLeft(bytes) + "cardODid" -> jsApi.ankiGetCardODid(bytes) + "cardODue" -> jsApi.ankiGetCardODue(bytes) + "cardQueue" -> jsApi.ankiGetCardQueue(bytes) + "cardLapses" -> jsApi.ankiGetCardLapses(bytes) + "cardDue" -> jsApi.ankiGetCardDue(bytes) + "deckName" -> jsApi.ankiGetDeckName(bytes) + "isActiveNetworkMetered" -> jsApi.ankiIsActiveNetworkMetered(bytes) "ttsSetLanguage" -> jsApi.ankiTtsSetLanguage(bytes) "ttsSpeak" -> jsApi.ankiTtsSpeak(bytes) - "ttsIsSpeaking" -> jsApi.ankiTtsIsSpeaking() + "ttsIsSpeaking" -> jsApi.ankiTtsIsSpeaking(bytes) "ttsSetPitch" -> jsApi.ankiTtsSetPitch(bytes) "ttsSetSpeechRate" -> jsApi.ankiTtsSetSpeechRate(bytes) - "ttsFieldModifierIsAvailable" -> jsApi.ankiTtsFieldModifierIsAvailable() - "ttsStop" -> jsApi.ankiTtsStop() - "nextTime1" -> jsApi.ankiGetNextTime1() - "nextTime2" -> jsApi.ankiGetNextTime2() - "nextTime3" -> jsApi.ankiGetNextTime3() - "nextTime4" -> jsApi.ankiGetNextTime4() + "ttsFieldModifierIsAvailable" -> jsApi.ankiTtsFieldModifierIsAvailable(bytes) + "ttsStop" -> jsApi.ankiTtsStop(bytes) + "nextTime1" -> jsApi.ankiGetNextTime1(bytes) + "nextTime2" -> jsApi.ankiGetNextTime2(bytes) + "nextTime3" -> jsApi.ankiGetNextTime3(bytes) + "nextTime4" -> jsApi.ankiGetNextTime4(bytes) "searchCard" -> jsApi.ankiSearchCard(bytes) "searchCardWithCallback" -> jsApi.ankiSearchCardWithCallback(bytes) "buryCard" -> jsApi.ankiBuryCard(bytes) @@ -142,11 +142,11 @@ class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiSer "suspendNote" -> jsApi.ankiSuspendNote(bytes) "setCardDue" -> jsApi.ankiSetCardDue(bytes) "resetProgress" -> jsApi.ankiResetProgress(bytes) - "isDisplayingAnswer" -> jsApi.ankiIsDisplayingAnswer() - "addTagToCard" -> jsApi.ankiAddTagToCard() - "isInFullscreen" -> jsApi.ankiIsInFullscreen() - "isTopbarShown" -> jsApi.ankiIsTopbarShown() - "isInNightMode" -> jsApi.ankiIsInNightMode() + "isDisplayingAnswer" -> jsApi.ankiIsDisplayingAnswer(bytes) + "addTagToCard" -> jsApi.ankiAddTagToCard(bytes) + "isInFullscreen" -> jsApi.ankiIsInFullscreen(bytes) + "isTopbarShown" -> jsApi.ankiIsTopbarShown(bytes) + "isInNightMode" -> jsApi.ankiIsInNightMode(bytes) "enableHorizontalScrollbar" -> jsApi.ankiEnableHorizontalScrollbar(bytes) "enableVerticalScrollbar" -> jsApi.ankiEnableVerticalScrollbar(bytes) "toggleFlag" -> jsApi.ankiToggleFlag(bytes) From 79956b425be391baa8c6f575348663e182122410 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:10:22 +0800 Subject: [PATCH 04/10] update junit test for js api --- .../java/com/ichi2/anki/AnkiDroidJsAPITest.kt | 108 +++++++++--------- .../test/java/com/ichi2/anki/ReviewerTest.kt | 9 +- 2 files changed, 60 insertions(+), 57 deletions(-) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt index 1f66f3059916..313592cb3ab1 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt @@ -52,7 +52,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { "{\"setCardDue\":true,\"suspendNote\":true,\"markCard\":true,\"suspendCard\":true,\"buryCard\":true,\"toggleFlag\":true,\"buryNote\":true}" waitForAsyncTasksToComplete() - assertThat(javaScriptFunction.init(jsApiContract()).decodeToString(), equalTo(expected)) + assertThat(javaScriptFunction.init(Companion.jsApiContract()).decodeToString(), equalTo(expected)) } @Test @@ -73,19 +73,19 @@ class AnkiDroidJsAPITest : RobolectricTest() { waitForAsyncTasksToComplete() assertThat( - javaScriptFunction.ankiGetNextTime1().decodeToString().withoutUnicodeIsolation(), + javaScriptFunction.ankiGetNextTime1(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), equalTo("<1m") ) assertThat( - javaScriptFunction.ankiGetNextTime2().decodeToString().withoutUnicodeIsolation(), + javaScriptFunction.ankiGetNextTime2(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), equalTo("<6m") ) assertThat( - javaScriptFunction.ankiGetNextTime3().decodeToString().withoutUnicodeIsolation(), + javaScriptFunction.ankiGetNextTime3(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), equalTo("<10m") ) assertThat( - javaScriptFunction.ankiGetNextTime4().decodeToString().withoutUnicodeIsolation(), + javaScriptFunction.ankiGetNextTime4(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), equalTo("4d") ) } @@ -110,87 +110,87 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Card Did assertThat( - javaScriptFunction.ankiGetCardDid().decodeToString().toLong(), + javaScriptFunction.ankiGetCardDid(Companion.jsApiContract()).decodeToString().toLong(), equalTo(currentCard.did) ) // Card Id assertThat( - javaScriptFunction.ankiGetCardId().decodeToString().toLong(), + javaScriptFunction.ankiGetCardId(Companion.jsApiContract()).decodeToString().toLong(), equalTo(currentCard.id) ) // Card Nid assertThat( - javaScriptFunction.ankiGetCardNid().decodeToString().toLong(), + javaScriptFunction.ankiGetCardNid(Companion.jsApiContract()).decodeToString().toLong(), equalTo(currentCard.nid) ) // Card ODid assertThat( - javaScriptFunction.ankiGetCardODid().decodeToString().toLong(), + javaScriptFunction.ankiGetCardODid(Companion.jsApiContract()).decodeToString().toLong(), equalTo(currentCard.oDid) ) // Card Type assertThat( - javaScriptFunction.ankiGetCardType().decodeToString().toInt(), + javaScriptFunction.ankiGetCardType(Companion.jsApiContract()).decodeToString().toInt(), equalTo(currentCard.type) ) // Card ODue assertThat( - javaScriptFunction.ankiGetCardODue().decodeToString().toLong(), + javaScriptFunction.ankiGetCardODue(Companion.jsApiContract()).decodeToString().toLong(), equalTo(currentCard.oDue) ) // Card Due assertThat( - javaScriptFunction.ankiGetCardDue().decodeToString().toLong(), + javaScriptFunction.ankiGetCardDue(Companion.jsApiContract()).decodeToString().toLong(), equalTo(currentCard.due) ) // Card Factor assertThat( - javaScriptFunction.ankiGetCardFactor().decodeToString().toInt(), + javaScriptFunction.ankiGetCardFactor(Companion.jsApiContract()).decodeToString().toInt(), equalTo(currentCard.factor) ) // Card Lapses assertThat( - javaScriptFunction.ankiGetCardLapses().decodeToString().toInt(), + javaScriptFunction.ankiGetCardLapses(Companion.jsApiContract()).decodeToString().toInt(), equalTo(currentCard.lapses) ) // Card Ivl assertThat( - javaScriptFunction.ankiGetCardInterval().decodeToString().toInt(), + javaScriptFunction.ankiGetCardInterval(Companion.jsApiContract()).decodeToString().toInt(), equalTo(currentCard.ivl) ) // Card mod assertThat( - javaScriptFunction.ankiGetCardMod().decodeToString().toLong(), + javaScriptFunction.ankiGetCardMod(Companion.jsApiContract()).decodeToString().toLong(), equalTo(currentCard.mod) ) // Card Queue assertThat( - javaScriptFunction.ankiGetCardQueue().decodeToString().toInt(), + javaScriptFunction.ankiGetCardQueue(Companion.jsApiContract()).decodeToString().toInt(), equalTo(currentCard.queue) ) // Card Reps assertThat( - javaScriptFunction.ankiGetCardReps().decodeToString().toInt(), + javaScriptFunction.ankiGetCardReps(Companion.jsApiContract()).decodeToString().toInt(), equalTo(currentCard.reps) ) // Card left assertThat( - javaScriptFunction.ankiGetCardLeft().decodeToString().toInt(), + javaScriptFunction.ankiGetCardLeft(Companion.jsApiContract()).decodeToString().toInt(), equalTo(currentCard.left) ) // Card Flag - assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), equalTo(0)) + assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(0)) reviewer.currentCard!!.setFlag(1) - assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), equalTo(1)) + assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(1)) // Card Mark assertThat( - javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), + javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(false) ) reviewer.currentCard!!.note().addTag("marked") - assertThat(javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), equalTo(true)) + assertThat(javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(true)) } @Test @@ -210,28 +210,28 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Displaying question assertThat( - javaScriptFunction.ankiIsDisplayingAnswer().decodeToString().toBoolean(), + javaScriptFunction.ankiIsDisplayingAnswer(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(reviewer.isDisplayingAnswer) ) reviewer.displayCardAnswer() assertThat( - javaScriptFunction.ankiIsDisplayingAnswer().decodeToString().toBoolean(), + javaScriptFunction.ankiIsDisplayingAnswer(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(reviewer.isDisplayingAnswer) ) // Full Screen assertThat( - javaScriptFunction.ankiIsInFullscreen().decodeToString().toBoolean(), + javaScriptFunction.ankiIsInFullscreen(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(reviewer.isFullscreen) ) // Top bar assertThat( - javaScriptFunction.ankiIsTopbarShown().decodeToString().toBoolean(), + javaScriptFunction.ankiIsTopbarShown(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(reviewer.prefShowTopbar) ) // Night Mode assertThat( - javaScriptFunction.ankiIsInNightMode().decodeToString().toBoolean(), + javaScriptFunction.ankiIsInNightMode(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(reviewer.isInNightMode) ) } @@ -257,23 +257,23 @@ class AnkiDroidJsAPITest : RobolectricTest() { // --------------- // Before marking card assertThat( - javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), + javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(false) ) // get card mark status for test - javaScriptFunction.ankiMarkCard(jsApiContract()) - assertThat(javaScriptFunction.ankiGetCardMark().decodeToString().toBoolean(), equalTo(true)) + javaScriptFunction.ankiMarkCard(Companion.jsApiContract()) + assertThat(javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(true)) // --------------- // Card flag test // --------------- // before toggling flag - assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), equalTo(0)) + assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(0)) // call javascript function defined in card.js to toggle flag - javaScriptFunction.ankiToggleFlag(jsApiContract("red")) - assertThat(javaScriptFunction.ankiGetCardFlag().decodeToString().toInt(), equalTo(1)) + javaScriptFunction.ankiToggleFlag(Companion.jsApiContract("red")) + assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(1)) } // TODO - update test @@ -297,14 +297,14 @@ class AnkiDroidJsAPITest : RobolectricTest() { val reviewer: Reviewer = startReviewer() val javaScriptFunction = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(jsApiContract()) + javaScriptFunction.init(Companion.jsApiContract()) waitForAsyncTasksToComplete() // ---------- // Bury Card // ---------- // call script to bury current card - javaScriptFunction.ankiBuryCard(jsApiContract()) + javaScriptFunction.ankiBuryCard(Companion.jsApiContract()) waitForAsyncTasksToComplete() // count number of notes @@ -315,7 +315,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Bury Note // ---------- // call script to bury current note - javaScriptFunction.ankiBuryNote(jsApiContract()) + javaScriptFunction.ankiBuryNote(Companion.jsApiContract()) // count number of notes assertThat(sched.cardCount(), equalTo(3)) @@ -324,7 +324,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Suspend Card // ------------- // call script to suspend current card - javaScriptFunction.ankiSuspendCard(jsApiContract()) + javaScriptFunction.ankiSuspendCard(Companion.jsApiContract()) // count number of notes assertThat(sched.cardCount(), equalTo(2)) @@ -333,7 +333,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Suspend Note // ------------- // call script to suspend current note - javaScriptFunction.ankiSuspendNote(jsApiContract()) + javaScriptFunction.ankiSuspendNote(Companion.jsApiContract()) // count number of notes assertThat(sched.cardCount(), equalTo(1)) @@ -360,14 +360,14 @@ class AnkiDroidJsAPITest : RobolectricTest() { val javaScriptFunction = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(jsApiContract()) + javaScriptFunction.init(Companion.jsApiContract()) // get card id for testing due - val cardId = javaScriptFunction.ankiGetCardId().decodeToString().toLong() + val cardId = javaScriptFunction.ankiGetCardId(Companion.jsApiContract()).decodeToString().toLong() // test that card rescheduled for 15 days interval and returned true assertTrue( "Card rescheduled, so returns true", - javaScriptFunction.ankiSetCardDue(jsApiContract("15")).decodeToString().toBoolean() + javaScriptFunction.ankiSetCardDue(Companion.jsApiContract("15")).decodeToString().toBoolean() ) waitForAsyncTasksToComplete() @@ -377,14 +377,6 @@ class AnkiDroidJsAPITest : RobolectricTest() { assertEquals("Card is rescheduled", 15L + col.sched.today, cardAfterRescheduleCards.due) } - private fun jsApiContract(data: String = ""): ByteArray { - val jsonObject = JSONObject() - jsonObject.put("version", "0.0.2") - jsonObject.put("developer", "test@example.com") - jsonObject.put("data", data) - return jsonObject.toString().toByteArray() - } - @Test fun ankiResetProgressTest() = runTest { val n = addNoteUsingBasicModel("Front", "Back") @@ -407,14 +399,14 @@ class AnkiDroidJsAPITest : RobolectricTest() { val javaScriptFunction = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(jsApiContract()) + javaScriptFunction.init(Companion.jsApiContract()) // get card id for testing due - val cardId = javaScriptFunction.ankiGetCardId().decodeToString().toLong() + val cardId = javaScriptFunction.ankiGetCardId(Companion.jsApiContract()).decodeToString().toLong() // test that card reset assertTrue( "Card progress reset", - javaScriptFunction.ankiResetProgress(jsApiContract()).decodeToString().toBoolean() + javaScriptFunction.ankiResetProgress(Companion.jsApiContract()).decodeToString().toBoolean() ) waitForAsyncTasksToComplete() @@ -425,4 +417,14 @@ class AnkiDroidJsAPITest : RobolectricTest() { 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 { + val jsonObject = JSONObject() + jsonObject.put("version", "0.0.2") + jsonObject.put("developer", "test@example.com") + jsonObject.put("data", data) + return jsonObject.toString().toByteArray() + } + } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt index c26d0eb6322a..28a5e0297e22 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt @@ -21,6 +21,7 @@ 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.jsApiContract import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.cardviewer.ViewerCommand.FLIP_OR_ANSWER_EASE1 import com.ichi2.anki.cardviewer.ViewerCommand.MARK @@ -238,7 +239,7 @@ class ReviewerTest : RobolectricTest() { val javaScriptFunction = reviewer.javaScriptFunction() waitForAsyncTasksToComplete() - assertThat(javaScriptFunction.ankiGetDeckName().decodeToString(), equalTo("B")) + assertThat(javaScriptFunction.ankiGetDeckName(jsApiContract()).decodeToString(), equalTo("B")) } @Ignore("needs update for v3") @@ -327,9 +328,9 @@ class ReviewerTest : RobolectricTest() { private fun assertCounts(r: Reviewer, newCount: Int, stepCount: Int, revCount: Int) = runTest { val jsApi = r.javaScriptFunction() val countList = listOf( - jsApi.ankiGetNewCardCount().decodeToString().toInt(), - jsApi.ankiGetLrnCardCount().decodeToString().toInt(), - jsApi.ankiGetRevCardCount().decodeToString().toInt() + jsApi.ankiGetNewCardCount(jsApiContract()).decodeToString().toInt(), + jsApi.ankiGetLrnCardCount(jsApiContract()).decodeToString().toInt(), + jsApi.ankiGetRevCardCount(jsApiContract()).decodeToString().toInt() ) val expected = listOf( From d834552d7f53f1e5cde31081956257aaecc60840 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:04:19 +0800 Subject: [PATCH 05/10] move api to one class and update tests --- .../com/ichi2/anki/AbstractFlashcardViewer.kt | 4 + .../java/com/ichi2/anki/AnkiDroidJsAPI.kt | 534 +++++++----------- .../src/main/java/com/ichi2/anki/Reviewer.kt | 178 +----- .../java/com/ichi2/anki/ReviewerServer.kt | 63 +-- .../java/com/ichi2/anki/AnkiDroidJsAPITest.kt | 188 +++--- .../test/java/com/ichi2/anki/ReviewerTest.kt | 15 +- 6 files changed, 305 insertions(+), 677 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 3292a0f112c9..c596e9352996 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -2512,6 +2512,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 3cc7ff8d8aad..f987c02ea809 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -25,13 +25,14 @@ import android.net.Uri import com.github.zafarkhaja.semver.Version import com.google.android.material.snackbar.Snackbar import com.ichi2.anim.ActivityTransitionAnimation +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 @@ -141,7 +142,7 @@ 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) { 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) @@ -199,11 +200,11 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } } - protected fun getJsApiListMap(): HashMap { + private fun getJsApiListMap(): HashMap { return mJsApiListMap } - suspend fun init(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + fun init(byteArray: ByteArray): ByteArray { var apiStatusJson = "" try { checkJsApiContract(byteArray) @@ -214,284 +215,133 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { activity.showSnackbar(context.getString(R.string.invalid_json_data, j.localizedMessage)) } } - convertToByteArray(apiStatusJson) + return convertToByteArray(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. - open suspend fun ankiGetNewCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - convertToByteArray(-1) - } - - open suspend fun ankiGetLrnCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - convertToByteArray(-1) - } - - open suspend fun ankiGetRevCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - convertToByteArray(-1) - } - - open suspend fun ankiGetETA(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - convertToByteArray(-1) - } - - suspend fun ankiGetCardMark(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.note().hasTag("marked")) - } - - suspend fun ankiGetCardFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.userFlag()) - } - - // behavior change ankiGetNextTime1...4 - open suspend fun ankiGetNextTime1(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(activity.easeButton1!!.nextTime) - } - - open suspend fun ankiGetNextTime2(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(activity.easeButton2!!.nextTime) - } - - open suspend fun ankiGetNextTime3(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(activity.easeButton3!!.nextTime) - } - - open suspend fun ankiGetNextTime4(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(activity.easeButton4!!.nextTime) - } - - suspend fun ankiGetCardReps(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.reps) - } - - suspend fun ankiGetCardInterval(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.ivl) - } - - /** Returns the ease as an int (percentage * 10). Default: 2500 (250%). Minimum: 1300 (130%) */ - suspend fun ankiGetCardFactor(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.factor) - } - - /** Returns the last modified time as a Unix timestamp in seconds. Example: 1477384099 */ - suspend fun ankiGetCardMod(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.mod) - } - - /** Returns the ID of the card. Example: 1477380543053 */ - suspend fun ankiGetCardId(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.id) - } - - /** Returns the ID of the note which generated the card. Example: 1590418157630 */ - suspend fun ankiGetCardNid(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.nid) - } - - @CARD_TYPE - suspend fun ankiGetCardType(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.type) - } - - /** Returns the ID of the deck which contains the card. Example: 1595967594978 */ - suspend fun ankiGetCardDid(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.did) - } - - suspend fun ankiGetCardLeft(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.left) - } - - /** Returns the ID of the home deck for the card if it is filtered, or 0 if not filtered. Example: 1595967594978 */ - suspend fun ankiGetCardODid(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.oDid) - } - - suspend fun ankiGetCardODue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.oDue) - } - - @CARD_QUEUE - suspend fun ankiGetCardQueue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.queue) - } - - suspend fun ankiGetCardLapses(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.lapses) - } - - suspend fun ankiGetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(currentCard.due) - } - - suspend fun ankiIsInFullscreen(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(activity.isFullscreen) - } - - suspend fun ankiIsTopbarShown(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(activity.prefShowTopbar) - } - - suspend fun ankiIsInNightMode(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(activity.isInNightMode) - } - - suspend fun ankiIsDisplayingAnswer(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) + /** + * Handle js api request, + * some of the methods are overriden in Reviewer.kt and default values are returned. + * @param methodName + * @param bytes + * @return + */ + open suspend fun handleJsApiRequest(methodName: String, bytes: ByteArray, isReviewer: Boolean) = withContext(Dispatchers.Main) { + val data = checkJsApiContract(bytes) + // if api not init or is api not called from reviewer then return default -1 + // also other action will not be modified + if (!data.first or !isReviewer) { + return@withContext convertToByteArray(-1) } - convertToByteArray(activity.isDisplayingAnswer) - } - suspend fun ankiGetDeckName(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) + val cardDataForJsAPI = activity.getCardDataForJsApi() + + return@withContext when (methodName) { + "init" -> init(bytes) + "newCardCount" -> convertToByteArray(cardDataForJsAPI.newCardCount) + "lrnCardCount" -> convertToByteArray(cardDataForJsAPI.lrnCardCount) + "revCardCount" -> convertToByteArray(cardDataForJsAPI.revCardCount) + "eta" -> convertToByteArray(cardDataForJsAPI.eta) + "nextTime1" -> convertToByteArray(cardDataForJsAPI.nextTime1) + "nextTime2" -> convertToByteArray(cardDataForJsAPI.nextTime2) + "nextTime3" -> convertToByteArray(cardDataForJsAPI.nextTime3) + "nextTime4" -> convertToByteArray(cardDataForJsAPI.nextTime4) + "toggleFlag" -> ankiToggleFlag(bytes) + "markCard" -> ankiMarkCard(bytes) + "buryCard" -> ankiBuryCard() + "buryNote" -> ankiBuryNote() + "suspendCard" -> ankiSuspendCard() + "suspendNote" -> ankiSuspendNote() + "setCardDue" -> ankiSetCardDue(bytes) + "resetProgress" -> ankiResetProgress(bytes) + "cardMark" -> convertToByteArray(currentCard.note().hasTag("marked")) + "cardFlag" -> convertToByteArray(currentCard.userFlag()) + "cardReps" -> convertToByteArray(currentCard.reps) + "cardInterval" -> convertToByteArray(currentCard.ivl) + "cardFactor" -> convertToByteArray(currentCard.factor) + "cardMod" -> convertToByteArray(currentCard.mod) + "cardId" -> convertToByteArray(currentCard.id) + "cardNid" -> convertToByteArray(currentCard.nid) + "cardType" -> convertToByteArray(currentCard.type) + "cardDid" -> convertToByteArray(currentCard.did) + "cardLeft" -> convertToByteArray(currentCard.left) + "cardODid" -> convertToByteArray(currentCard.oDid) + "cardODue" -> convertToByteArray(currentCard.oDue) + "cardQueue" -> convertToByteArray(currentCard.queue) + "cardLapses" -> convertToByteArray(currentCard.lapses) + "cardDue" -> convertToByteArray(currentCard.due) + "deckName" -> convertToByteArray(Decks.basename(activity.getColUnsafe.decks.name(currentCard.did))) + "isActiveNetworkMetered" -> convertToByteArray(NetworkUtils.isActiveNetworkMetered()) + "ttsSetLanguage" -> convertToByteArray(mTalker.setLanguage(data.second)) + "ttsSpeak" -> ankiTtsSpeak(bytes) + "ttsIsSpeaking" -> convertToByteArray(mTalker.isSpeaking) + "ttsSetPitch" -> convertToByteArray(mTalker.setPitch(data.second.toFloat())) + "ttsSetSpeechRate" -> convertToByteArray(mTalker.setSpeechRate(data.second.toFloat())) + "ttsFieldModifierIsAvailable" -> + // Know if {{tts}} is supported - issue #10443 + // Return false for now + convertToByteArray(false) + "ttsStop" -> convertToByteArray(mTalker.stop()) + "searchCard" -> ankiSearchCard(bytes) + "searchCardWithCallback" -> ankiSearchCardWithCallback(bytes) + "isDisplayingAnswer" -> convertToByteArray(activity.isDisplayingAnswer) + "addTagToCard" -> { + activity.runOnUiThread { activity.showTagsDialog() } + convertToByteArray(true) + } + "isInFullscreen" -> convertToByteArray(activity.isFullscreen) + "isTopbarShown" -> convertToByteArray(activity.prefShowTopbar) + "isInNightMode" -> convertToByteArray(activity.isInNightMode) + "enableHorizontalScrollbar" -> { + activity.webView!!.isHorizontalScrollBarEnabled = data.second.toBoolean() + convertToByteArray(true) + } + "enableVerticalScrollbar" -> { + activity.webView!!.isVerticalScrollBarEnabled = data.second.toBoolean() + convertToByteArray(true) + } + else -> { + throw Exception("unhandled request: $methodName") + } } - convertToByteArray(Decks.basename(activity.getColUnsafe.decks.name(currentCard.did))) } - suspend fun ankiBuryCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - if (!isInit(AnkiDroidJsAPIConstants.BURY_CARD, AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryCard)) { + private suspend fun ankiBuryCard(): ByteArray = withContext(Dispatchers.Main) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.BURY_CARD]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryCard) return@withContext convertToByteArray(false) } convertToByteArray(activity.buryCard()) } - suspend fun ankiBuryNote(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - if (!isInit(AnkiDroidJsAPIConstants.BURY_NOTE, AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryNote)) { + private suspend fun ankiBuryNote(): ByteArray = withContext(Dispatchers.Main) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.BURY_NOTE]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryNote) return@withContext convertToByteArray(false) } convertToByteArray(activity.buryNote()) } - suspend fun ankiSuspendCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - if (!isInit(AnkiDroidJsAPIConstants.SUSPEND_CARD, AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendCard)) { + private suspend fun ankiSuspendCard(): ByteArray = withContext(Dispatchers.Main) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.SUSPEND_CARD]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendCard) return@withContext convertToByteArray(false) } convertToByteArray(activity.suspendCard()) } - suspend fun ankiSuspendNote(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - if (!isInit(AnkiDroidJsAPIConstants.SUSPEND_NOTE, AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendNote)) { + private suspend fun ankiSuspendNote(): ByteArray = withContext(Dispatchers.Main) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.SUSPEND_NOTE]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendNote) return@withContext convertToByteArray(false) } convertToByteArray(activity.suspendNote()) } - suspend fun ankiAddTagToCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - activity.runOnUiThread { activity.showTagsDialog() } - convertToByteArray(true) - } - - suspend fun ankiSearchCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + private suspend fun ankiSearchCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { val data = checkJsApiContract(byteArray) if (!data.first) { return@withContext convertToByteArray(false) @@ -504,25 +354,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { convertToByteArray(true) } - suspend fun ankiIsActiveNetworkMetered(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(NetworkUtils.isActiveNetworkMetered()) - } - - // Know if {{tts}} is supported - issue #10443 - // Return false for now - suspend fun ankiTtsFieldModifierIsAvailable(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(false) - } - - suspend fun ankiTtsSpeak(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + private suspend fun ankiTtsSpeak(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { val data = checkJsApiContract(byteArray) if (!data.first) { return@withContext convertToByteArray(-1) @@ -533,65 +365,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { convertToByteArray(mTalker.speak(text, queueMode)) } - suspend fun ankiTtsSetLanguage(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(mTalker.setLanguage(data.second)) - } - - suspend fun ankiTtsSetPitch(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(mTalker.setPitch(data.second.toFloat())) - } - - suspend fun ankiTtsSetSpeechRate(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(mTalker.setSpeechRate(data.second.toFloat())) - } - - suspend fun ankiTtsIsSpeaking(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(mTalker.isSpeaking) - } - - suspend fun ankiTtsStop(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - convertToByteArray(mTalker.stop()) - } - - suspend fun ankiEnableHorizontalScrollbar(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - activity.webView!!.isHorizontalScrollBarEnabled = data.second.toBoolean() - convertToByteArray(true) - } - - suspend fun ankiEnableVerticalScrollbar(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - activity.webView!!.isVerticalScrollBarEnabled = data.second.toBoolean() - convertToByteArray(true) - } - - suspend fun ankiSearchCardWithCallback(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + private suspend fun ankiSearchCardWithCallback(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { val data = checkJsApiContract(byteArray) if (!data.first) { return@withContext convertToByteArray(false) @@ -633,27 +407,103 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { convertToByteArray(true) } - open suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - // the function is overridden in Reviewer.kt - // it may be called in previewer so just return true value here + private suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val data = checkJsApiContract(byteArray) + val daysInt = data.second.toInt() + val apiList = getJsApiListMap() + if (!apiList[AnkiDroidJsAPIConstants.SET_CARD_DUE]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) + return@withContext convertToByteArray(false) + } + + if (daysInt < 0 || daysInt > 9999) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSetDue) + convertToByteArray(false) + } + + val cardIds = listOf(currentCard.id) + activity.launchCatchingTask { + activity.rescheduleCards(cardIds, daysInt) + } convertToByteArray(true) } - open suspend fun ankiResetProgress(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - // the function is overridden in Reviewer.kt - // it may be called in previewer so just return true value here + private suspend fun ankiResetProgress(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) + val apiList = getJsApiListMap() + if (!apiList[AnkiDroidJsAPIConstants.RESET_PROGRESS]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) + return@withContext convertToByteArray(false) + } + val cardIds = listOf(currentCard.id) + activity.launchCatchingTask { + activity.resetCards(cardIds) + } convertToByteArray(true) } - open suspend fun ankiMarkCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - // the function is overridden in Reviewer.kt - // it may be called in previewer so just return true value here + private suspend fun ankiMarkCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + checkJsApiContract(byteArray) + val apiList = getJsApiListMap() + if (!apiList[AnkiDroidJsAPIConstants.MARK_CARD]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) + return@withContext convertToByteArray(false) + } + + activity.executeCommand(ViewerCommand.MARK) convertToByteArray(true) } - open suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - // the function is overridden in Reviewer.kt - // it may be called in previewer so just return true value here + private suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { + val flag = checkJsApiContract(byteArray).second + // flag card (blue, green, orange, red) using javascript from AnkiDroid webview + val apiList = getJsApiListMap() + if (!apiList[AnkiDroidJsAPIConstants.TOGGLE_FLAG]!!) { + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) + return@withContext convertToByteArray(false) + } + + when (flag) { + "none" -> { + activity.executeCommand(ViewerCommand.UNSET_FLAG) + } + "red" -> { + activity.executeCommand(ViewerCommand.TOGGLE_FLAG_RED) + } + "orange" -> { + activity.executeCommand(ViewerCommand.TOGGLE_FLAG_ORANGE) + } + "green" -> { + activity.executeCommand(ViewerCommand.TOGGLE_FLAG_GREEN) + } + "blue" -> { + activity.executeCommand(ViewerCommand.TOGGLE_FLAG_BLUE) + } + "pink" -> { + activity.executeCommand(ViewerCommand.TOGGLE_FLAG_PINK) + } + "turquoise" -> { + activity.executeCommand(ViewerCommand.TOGGLE_FLAG_TURQUOISE) + } + "purple" -> { + activity.executeCommand(ViewerCommand.TOGGLE_FLAG_PURPLE) + } + else -> { + Timber.d("No such Flag found.") + convertToByteArray(false) + } + } convertToByteArray(true) } + + open class CardDataForJsApi { + var newCardCount = "" + var lrnCardCount = "" + var revCardCount = "" + var eta = -1 + var nextTime1 = "" + var nextTime2 = "" + var nextTime3 = "" + var nextTime4 = "" + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 5fbf7933a449..0264e4de3e88 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -46,12 +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.MARK_CARD -import com.ichi2.anki.AnkiDroidJsAPIConstants.RESET_PROGRESS -import com.ichi2.anki.AnkiDroidJsAPIConstants.SET_CARD_DUE -import com.ichi2.anki.AnkiDroidJsAPIConstants.TOGGLE_FLAG -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 @@ -91,8 +85,6 @@ import com.ichi2.utils.HandlerUtils.getDefaultLooper import com.ichi2.utils.Permissions.canRecordAudio import com.ichi2.utils.ViewGroupUtils.setRenderWorkaround import com.ichi2.widget.WidgetStatus.updateInBackground -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.util.function.Consumer @@ -1561,162 +1553,20 @@ open class Reviewer : } override fun javaScriptFunction(): AnkiDroidJsAPI { - return ReviewerJavaScriptFunction(this) - } - - inner class ReviewerJavaScriptFunction(activity: AbstractFlashcardViewer) : AnkiDroidJsAPI(activity) { - override suspend fun ankiGetNewCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(mNewCount.toString()) - } - - override suspend fun ankiGetLrnCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(mLrnCount.toString()) - } - - override suspend fun ankiGetRevCardCount(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(mRevCount.toString()) - } - - override suspend fun ankiGetETA(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(mEta) - } - - override suspend fun ankiGetNextTime1(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(easeButton1!!.nextTime) - } - - override suspend fun ankiGetNextTime2(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(easeButton2!!.nextTime) - } - - override suspend fun ankiGetNextTime3(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(easeButton3!!.nextTime) - } - - override suspend fun ankiGetNextTime4(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - convertToByteArray(easeButton4!!.nextTime) - } - - override suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - val daysInt = data.second.toInt() - val apiList = getJsApiListMap() - if (!apiList[SET_CARD_DUE]!!) { - showDeveloperContact(ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - - if (daysInt < 0 || daysInt > 9999) { - showDeveloperContact(ankiJsErrorCodeSetDue) - convertToByteArray(false) - } - - val cardIds = listOf(currentCard!!.id) - launchCatchingTask { - rescheduleCards(cardIds, daysInt) - } - convertToByteArray(true) - } - - override suspend fun ankiResetProgress(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - val apiList = getJsApiListMap() - if (!apiList[RESET_PROGRESS]!!) { - showDeveloperContact(ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - val cardIds = listOf(currentCard!!.id) - launchCatchingTask { - resetCards(cardIds) - } - convertToByteArray(true) - } - - override suspend fun ankiMarkCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - val apiList = getJsApiListMap() - if (!apiList[MARK_CARD]!!) { - showDeveloperContact(ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - - executeCommand(ViewerCommand.MARK) - convertToByteArray(true) - } - - override suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val flag = checkJsApiContract(byteArray).second - // flag card (blue, green, orange, red) using javascript from AnkiDroid webview - val apiList = getJsApiListMap() - if (!apiList[TOGGLE_FLAG]!!) { - showDeveloperContact(ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - - when (flag) { - "none" -> { - executeCommand(ViewerCommand.UNSET_FLAG) - } - "red" -> { - executeCommand(ViewerCommand.TOGGLE_FLAG_RED) - } - "orange" -> { - executeCommand(ViewerCommand.TOGGLE_FLAG_ORANGE) - } - "green" -> { - executeCommand(ViewerCommand.TOGGLE_FLAG_GREEN) - } - "blue" -> { - executeCommand(ViewerCommand.TOGGLE_FLAG_BLUE) - } - "pink" -> { - executeCommand(ViewerCommand.TOGGLE_FLAG_PINK) - } - "turquoise" -> { - executeCommand(ViewerCommand.TOGGLE_FLAG_TURQUOISE) - } - "purple" -> { - executeCommand(ViewerCommand.TOGGLE_FLAG_PURPLE) - } - else -> { - Timber.d("No such Flag found.") - convertToByteArray(false) - } - } - convertToByteArray(true) - } + return AnkiDroidJsAPI(this) + } + + override fun getCardDataForJsApi(): AnkiDroidJsAPI.CardDataForJsApi { + val cardDataForJsAPI = AnkiDroidJsAPI.CardDataForJsApi() + cardDataForJsAPI.newCardCount = mNewCount.toString() + cardDataForJsAPI.lrnCardCount = mLrnCount.toString() + cardDataForJsAPI.revCardCount = mRevCount.toString() + cardDataForJsAPI.nextTime1 = easeButton1!!.nextTime + cardDataForJsAPI.nextTime2 = easeButton2!!.nextTime + cardDataForJsAPI.nextTime3 = easeButton3!!.nextTime + cardDataForJsAPI.nextTime4 = easeButton4!!.nextTime + cardDataForJsAPI.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 c5c15ee535ab..509a961375b6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt @@ -26,7 +26,7 @@ import java.io.FileInputStream class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiServer(activity) { var reviewerHtml: String = "" - val jsApi = if (activity is Reviewer) { + private val jsApi = if (activity is Reviewer) { reviewer().javaScriptFunction() } else { cardTemplatePreviewer().javaScriptFunction() @@ -79,7 +79,7 @@ class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiSer } if (uri.startsWith(ANKIDROID_JS_PREFIX)) { return buildResponse { - handleJsApiPostRequest(uri.substring(ANKIDROID_JS_PREFIX.length), inputBytes) + jsApi.handleJsApiRequest(uri.substring(ANKIDROID_JS_PREFIX.length), inputBytes, activity is Reviewer) } } } @@ -98,65 +98,6 @@ class ReviewerServer(activity: FragmentActivity, val mediaDir: String) : AnkiSer } } - private suspend fun handleJsApiPostRequest(methodName: String, bytes: ByteArray): ByteArray { - return when (methodName) { - "init" -> jsApi.init(bytes) - "newCardCount" -> jsApi.ankiGetNewCardCount(bytes) - "lrnCardCount" -> jsApi.ankiGetLrnCardCount(bytes) - "revCardCount" -> jsApi.ankiGetRevCardCount(bytes) - "eta" -> jsApi.ankiGetETA(bytes) - "cardMark" -> jsApi.ankiGetCardMark(bytes) - "cardFlag" -> jsApi.ankiGetCardFlag(bytes) - "cardReps" -> jsApi.ankiGetCardReps(bytes) - "cardInterval" -> jsApi.ankiGetCardInterval(bytes) - "cardFactor" -> jsApi.ankiGetCardFactor(bytes) - "cardMod" -> jsApi.ankiGetCardMod(bytes) - "cardId" -> jsApi.ankiGetCardId(bytes) - "cardNid" -> jsApi.ankiGetCardNid(bytes) - "cardType" -> jsApi.ankiGetCardType(bytes) - "cardDid" -> jsApi.ankiGetCardDid(bytes) - "cardLeft" -> jsApi.ankiGetCardLeft(bytes) - "cardODid" -> jsApi.ankiGetCardODid(bytes) - "cardODue" -> jsApi.ankiGetCardODue(bytes) - "cardQueue" -> jsApi.ankiGetCardQueue(bytes) - "cardLapses" -> jsApi.ankiGetCardLapses(bytes) - "cardDue" -> jsApi.ankiGetCardDue(bytes) - "deckName" -> jsApi.ankiGetDeckName(bytes) - "isActiveNetworkMetered" -> jsApi.ankiIsActiveNetworkMetered(bytes) - "ttsSetLanguage" -> jsApi.ankiTtsSetLanguage(bytes) - "ttsSpeak" -> jsApi.ankiTtsSpeak(bytes) - "ttsIsSpeaking" -> jsApi.ankiTtsIsSpeaking(bytes) - "ttsSetPitch" -> jsApi.ankiTtsSetPitch(bytes) - "ttsSetSpeechRate" -> jsApi.ankiTtsSetSpeechRate(bytes) - "ttsFieldModifierIsAvailable" -> jsApi.ankiTtsFieldModifierIsAvailable(bytes) - "ttsStop" -> jsApi.ankiTtsStop(bytes) - "nextTime1" -> jsApi.ankiGetNextTime1(bytes) - "nextTime2" -> jsApi.ankiGetNextTime2(bytes) - "nextTime3" -> jsApi.ankiGetNextTime3(bytes) - "nextTime4" -> jsApi.ankiGetNextTime4(bytes) - "searchCard" -> jsApi.ankiSearchCard(bytes) - "searchCardWithCallback" -> jsApi.ankiSearchCardWithCallback(bytes) - "buryCard" -> jsApi.ankiBuryCard(bytes) - "buryNote" -> jsApi.ankiBuryNote(bytes) - "suspendCard" -> jsApi.ankiSuspendCard(bytes) - "suspendNote" -> jsApi.ankiSuspendNote(bytes) - "setCardDue" -> jsApi.ankiSetCardDue(bytes) - "resetProgress" -> jsApi.ankiResetProgress(bytes) - "isDisplayingAnswer" -> jsApi.ankiIsDisplayingAnswer(bytes) - "addTagToCard" -> jsApi.ankiAddTagToCard(bytes) - "isInFullscreen" -> jsApi.ankiIsInFullscreen(bytes) - "isTopbarShown" -> jsApi.ankiIsTopbarShown(bytes) - "isInNightMode" -> jsApi.ankiIsInNightMode(bytes) - "enableHorizontalScrollbar" -> jsApi.ankiEnableHorizontalScrollbar(bytes) - "enableVerticalScrollbar" -> jsApi.ankiEnableVerticalScrollbar(bytes) - "toggleFlag" -> jsApi.ankiToggleFlag(bytes) - "markCard" -> jsApi.ankiMarkCard(bytes) - else -> { - throw Exception("unhandled request: $methodName") - } - } - } - private fun reviewer(): Reviewer { return (activity as Reviewer) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt index 313592cb3ab1..8ff73d77f26e 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt @@ -26,6 +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.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -52,7 +53,11 @@ class AnkiDroidJsAPITest : RobolectricTest() { "{\"setCardDue\":true,\"suspendNote\":true,\"markCard\":true,\"suspendCard\":true,\"buryCard\":true,\"toggleFlag\":true,\"buryNote\":true}" waitForAsyncTasksToComplete() - assertThat(javaScriptFunction.init(Companion.jsApiContract()).decodeToString(), equalTo(expected)) + assertThat( + javaScriptFunction.handleJsApiRequest("init", jsApiContract(), true) + .decodeToString(), + equalTo(expected) + ) } @Test @@ -66,26 +71,26 @@ class AnkiDroidJsAPITest : RobolectricTest() { decks.select(didA) val reviewer: Reviewer = startReviewer() - val javaScriptFunction = reviewer.javaScriptFunction() + val jsapi = reviewer.javaScriptFunction() reviewer.displayCardAnswer() waitForAsyncTasksToComplete() assertThat( - javaScriptFunction.ankiGetNextTime1(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), + getDataFromRequest("nextTime1", jsapi).withoutUnicodeIsolation(), equalTo("<1m") ) assertThat( - javaScriptFunction.ankiGetNextTime2(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), + getDataFromRequest("nextTime2", jsapi).withoutUnicodeIsolation(), equalTo("<6m") ) assertThat( - javaScriptFunction.ankiGetNextTime3(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), + getDataFromRequest("nextTime3", jsapi).withoutUnicodeIsolation(), equalTo("<10m") ) assertThat( - javaScriptFunction.ankiGetNextTime4(Companion.jsApiContract()).decodeToString().withoutUnicodeIsolation(), + getDataFromRequest("nextTime4", jsapi).withoutUnicodeIsolation(), equalTo("4d") ) } @@ -101,7 +106,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { decks.select(didA) val reviewer: Reviewer = startReviewer() - val javaScriptFunction = reviewer.javaScriptFunction() + val jsapi = reviewer.javaScriptFunction() reviewer.displayCardAnswer() waitForAsyncTasksToComplete() @@ -109,88 +114,43 @@ class AnkiDroidJsAPITest : RobolectricTest() { val currentCard = reviewer.currentCard!! // Card Did - assertThat( - javaScriptFunction.ankiGetCardDid(Companion.jsApiContract()).decodeToString().toLong(), - equalTo(currentCard.did) - ) + assertThat(getDataFromRequest("cardDid", jsapi).toLong(), equalTo(currentCard.did)) // Card Id - assertThat( - javaScriptFunction.ankiGetCardId(Companion.jsApiContract()).decodeToString().toLong(), - equalTo(currentCard.id) - ) + assertThat(getDataFromRequest("cardId", jsapi).toLong(), equalTo(currentCard.id)) // Card Nid - assertThat( - javaScriptFunction.ankiGetCardNid(Companion.jsApiContract()).decodeToString().toLong(), - equalTo(currentCard.nid) - ) + assertThat(getDataFromRequest("cardNid", jsapi).toLong(), equalTo(currentCard.nid)) // Card ODid - assertThat( - javaScriptFunction.ankiGetCardODid(Companion.jsApiContract()).decodeToString().toLong(), - equalTo(currentCard.oDid) - ) + assertThat(getDataFromRequest("cardODid", jsapi).toLong(), equalTo(currentCard.oDid)) // Card Type - assertThat( - javaScriptFunction.ankiGetCardType(Companion.jsApiContract()).decodeToString().toInt(), - equalTo(currentCard.type) - ) + assertThat(getDataFromRequest("cardType", jsapi).toInt(), equalTo(currentCard.type)) // Card ODue - assertThat( - javaScriptFunction.ankiGetCardODue(Companion.jsApiContract()).decodeToString().toLong(), - equalTo(currentCard.oDue) - ) + assertThat(getDataFromRequest("cardODue", jsapi).toLong(), equalTo(currentCard.oDue)) // Card Due - assertThat( - javaScriptFunction.ankiGetCardDue(Companion.jsApiContract()).decodeToString().toLong(), - equalTo(currentCard.due) - ) + assertThat(getDataFromRequest("cardDue", jsapi).toLong(), equalTo(currentCard.due)) // Card Factor - assertThat( - javaScriptFunction.ankiGetCardFactor(Companion.jsApiContract()).decodeToString().toInt(), - equalTo(currentCard.factor) - ) + assertThat(getDataFromRequest("cardFactor", jsapi).toInt(), equalTo(currentCard.factor)) // Card Lapses - assertThat( - javaScriptFunction.ankiGetCardLapses(Companion.jsApiContract()).decodeToString().toInt(), - equalTo(currentCard.lapses) - ) + assertThat(getDataFromRequest("cardLapses", jsapi).toInt(), equalTo(currentCard.lapses)) // Card Ivl - assertThat( - javaScriptFunction.ankiGetCardInterval(Companion.jsApiContract()).decodeToString().toInt(), - equalTo(currentCard.ivl) - ) + assertThat(getDataFromRequest("cardInterval", jsapi).toInt(), equalTo(currentCard.ivl)) // Card mod - assertThat( - javaScriptFunction.ankiGetCardMod(Companion.jsApiContract()).decodeToString().toLong(), - equalTo(currentCard.mod) - ) + assertThat(getDataFromRequest("cardMod", jsapi).toLong(), equalTo(currentCard.mod)) // Card Queue - assertThat( - javaScriptFunction.ankiGetCardQueue(Companion.jsApiContract()).decodeToString().toInt(), - equalTo(currentCard.queue) - ) + assertThat(getDataFromRequest("cardQueue", jsapi).toInt(), equalTo(currentCard.queue)) // Card Reps - assertThat( - javaScriptFunction.ankiGetCardReps(Companion.jsApiContract()).decodeToString().toInt(), - equalTo(currentCard.reps) - ) + assertThat(getDataFromRequest("cardReps", jsapi).toInt(), equalTo(currentCard.reps)) // Card left - assertThat( - javaScriptFunction.ankiGetCardLeft(Companion.jsApiContract()).decodeToString().toInt(), - equalTo(currentCard.left) - ) + assertThat(getDataFromRequest("cardLeft", jsapi).toInt(), equalTo(currentCard.left)) // Card Flag - assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(0)) + assertThat(getDataFromRequest("cardFlag", jsapi).toInt(), equalTo(0)) reviewer.currentCard!!.setFlag(1) - assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(1)) + assertThat(getDataFromRequest("cardFlag", jsapi).toInt(), equalTo(1)) // Card Mark - assertThat( - javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), - equalTo(false) - ) + assertThat(getDataFromRequest("cardMark", jsapi).toBoolean(), equalTo(false)) reviewer.currentCard!!.note().addTag("marked") - assertThat(javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(true)) + assertThat(getDataFromRequest("cardMark", jsapi).toBoolean(), equalTo(true)) } @Test @@ -204,34 +164,34 @@ class AnkiDroidJsAPITest : RobolectricTest() { decks.select(didA) val reviewer: Reviewer = startReviewer() - val javaScriptFunction = reviewer.javaScriptFunction() + val jsapi = reviewer.javaScriptFunction() waitForAsyncTasksToComplete() // Displaying question assertThat( - javaScriptFunction.ankiIsDisplayingAnswer(Companion.jsApiContract()).decodeToString().toBoolean(), + getDataFromRequest("isDisplayingAnswer", jsapi).toBoolean(), equalTo(reviewer.isDisplayingAnswer) ) reviewer.displayCardAnswer() assertThat( - javaScriptFunction.ankiIsDisplayingAnswer(Companion.jsApiContract()).decodeToString().toBoolean(), + getDataFromRequest("isDisplayingAnswer", jsapi).toBoolean(), equalTo(reviewer.isDisplayingAnswer) ) // Full Screen assertThat( - javaScriptFunction.ankiIsInFullscreen(Companion.jsApiContract()).decodeToString().toBoolean(), + getDataFromRequest("isInFullscreen", jsapi).toBoolean(), equalTo(reviewer.isFullscreen) ) // Top bar assertThat( - javaScriptFunction.ankiIsTopbarShown(Companion.jsApiContract()).decodeToString().toBoolean(), + getDataFromRequest("isTopbarShown", jsapi).toBoolean(), equalTo(reviewer.prefShowTopbar) ) // Night Mode assertThat( - javaScriptFunction.ankiIsInNightMode(Companion.jsApiContract()).decodeToString().toBoolean(), + getDataFromRequest("isInNightMode", jsapi).toBoolean(), equalTo(reviewer.isInNightMode) ) } @@ -248,7 +208,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { decks.select(didA) val reviewer: Reviewer = startReviewer() - val javaScriptFunction = reviewer.javaScriptFunction() + val jsapi = reviewer.javaScriptFunction() waitForAsyncTasksToComplete() @@ -256,24 +216,38 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Card mark test // --------------- // Before marking card - assertThat( - javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), - equalTo(false) - ) + assertFalse(getDataFromRequest("cardMark", jsapi).toBoolean()) - // get card mark status for test - javaScriptFunction.ankiMarkCard(Companion.jsApiContract()) - assertThat(javaScriptFunction.ankiGetCardMark(Companion.jsApiContract()).decodeToString().toBoolean(), equalTo(true)) + // Mark card + assertTrue( + jsapi.handleJsApiRequest("markCard", jsApiContract("true"), true) + .decodeToString().toBoolean() + ) + // After marking card + assertTrue(getDataFromRequest("cardMark", jsapi).toBoolean()) // --------------- // Card flag test // --------------- // before toggling flag - assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(0)) + assertThat( + jsapi.handleJsApiRequest("cardFlag", jsApiContract(), true) + .decodeToString().toInt(), + equalTo(0) + ) - // call javascript function defined in card.js to toggle flag - javaScriptFunction.ankiToggleFlag(Companion.jsApiContract("red")) - assertThat(javaScriptFunction.ankiGetCardFlag(Companion.jsApiContract()).decodeToString().toInt(), equalTo(1)) + // call javascript function to toggle flag + assertThat( + jsapi.handleJsApiRequest("toggleFlag", jsApiContract("red"), true) + .decodeToString().toBoolean(), + equalTo(true) + ) + // after toggling flag + assertThat( + jsapi.handleJsApiRequest("cardFlag", jsApiContract(), true) + .decodeToString().toInt(), + equalTo(1) + ) } // TODO - update test @@ -295,17 +269,16 @@ class AnkiDroidJsAPITest : RobolectricTest() { decks.select(didA) val reviewer: Reviewer = startReviewer() - val javaScriptFunction = reviewer.javaScriptFunction() + val jsapi = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(Companion.jsApiContract()) + jsapi.init(jsApiContract()) waitForAsyncTasksToComplete() // ---------- // Bury Card // ---------- // call script to bury current card - javaScriptFunction.ankiBuryCard(Companion.jsApiContract()) - waitForAsyncTasksToComplete() + assertTrue(getDataFromRequest("buryCard", jsapi).toBoolean()) // count number of notes val sched = reviewer.getColUnsafe @@ -315,7 +288,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Bury Note // ---------- // call script to bury current note - javaScriptFunction.ankiBuryNote(Companion.jsApiContract()) + assertTrue(getDataFromRequest("buryNote", jsapi).toBoolean()) // count number of notes assertThat(sched.cardCount(), equalTo(3)) @@ -324,7 +297,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Suspend Card // ------------- // call script to suspend current card - javaScriptFunction.ankiSuspendCard(Companion.jsApiContract()) + assertTrue(getDataFromRequest("suspendCard", jsapi).toBoolean()) // count number of notes assertThat(sched.cardCount(), equalTo(2)) @@ -333,7 +306,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Suspend Note // ------------- // call script to suspend current note - javaScriptFunction.ankiSuspendNote(Companion.jsApiContract()) + assertTrue(getDataFromRequest("suspendNote", jsapi).toBoolean()) // count number of notes assertThat(sched.cardCount(), equalTo(1)) @@ -358,16 +331,17 @@ class AnkiDroidJsAPITest : RobolectricTest() { val reviewer: Reviewer = startReviewer() waitForAsyncTasksToComplete() - val javaScriptFunction = reviewer.javaScriptFunction() + val jsapi = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(Companion.jsApiContract()) + jsapi.init(jsApiContract()) // get card id for testing due - val cardId = javaScriptFunction.ankiGetCardId(Companion.jsApiContract()).decodeToString().toLong() + val cardId = getDataFromRequest("cardId", jsapi).toLong() // test that card rescheduled for 15 days interval and returned true assertTrue( "Card rescheduled, so returns true", - javaScriptFunction.ankiSetCardDue(Companion.jsApiContract("15")).decodeToString().toBoolean() + jsapi.handleJsApiRequest("setCardDue", jsApiContract("15"), true) + .decodeToString().toBoolean() ) waitForAsyncTasksToComplete() @@ -397,17 +371,14 @@ class AnkiDroidJsAPITest : RobolectricTest() { val reviewer: Reviewer = startReviewer() waitForAsyncTasksToComplete() - val javaScriptFunction = reviewer.javaScriptFunction() + val jsapi = reviewer.javaScriptFunction() // init js api - javaScriptFunction.init(Companion.jsApiContract()) + jsapi.init(jsApiContract()) // get card id for testing due - val cardId = javaScriptFunction.ankiGetCardId(Companion.jsApiContract()).decodeToString().toLong() + val cardId = getDataFromRequest("cardId", jsapi).toLong() // test that card reset - assertTrue( - "Card progress reset", - javaScriptFunction.ankiResetProgress(Companion.jsApiContract()).decodeToString().toBoolean() - ) + assertTrue("Card progress reset", getDataFromRequest("resetProgress", jsapi).toBoolean()) waitForAsyncTasksToComplete() // verify that card progress reset @@ -418,6 +389,11 @@ class AnkiDroidJsAPITest : RobolectricTest() { assertEquals("Card type after reset", Consts.CARD_TYPE_NEW, cardAfterReset.type) } + private suspend fun getDataFromRequest(methodName: String, jsAPI: AnkiDroidJsAPI): String { + return jsAPI.handleJsApiRequest(methodName, jsApiContract(), true) + .decodeToString() + } + companion object { fun jsApiContract(data: String = ""): ByteArray { val jsonObject = JSONObject() diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt index 28a5e0297e22..cf699daea142 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReviewerTest.kt @@ -239,7 +239,11 @@ class ReviewerTest : RobolectricTest() { val javaScriptFunction = reviewer.javaScriptFunction() waitForAsyncTasksToComplete() - assertThat(javaScriptFunction.ankiGetDeckName(jsApiContract()).decodeToString(), equalTo("B")) + assertThat( + javaScriptFunction.handleJsApiRequest("deckName", jsApiContract(), true) + .decodeToString(), + equalTo("B") + ) } @Ignore("needs update for v3") @@ -328,9 +332,12 @@ class ReviewerTest : RobolectricTest() { private fun assertCounts(r: Reviewer, newCount: Int, stepCount: Int, revCount: Int) = runTest { val jsApi = r.javaScriptFunction() val countList = listOf( - jsApi.ankiGetNewCardCount(jsApiContract()).decodeToString().toInt(), - jsApi.ankiGetLrnCardCount(jsApiContract()).decodeToString().toInt(), - jsApi.ankiGetRevCardCount(jsApiContract()).decodeToString().toInt() + jsApi.handleJsApiRequest("newCardCount", jsApiContract(), true) + .decodeToString().toInt(), + jsApi.handleJsApiRequest("lrnCardCount", jsApiContract(), true) + .decodeToString().toInt(), + jsApi.handleJsApiRequest("revCardCount", jsApiContract(), true) + .decodeToString().toInt() ) val expected = listOf( From 3038eea11f75d00856f5fe161338e1fde52d46b2 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:21:38 +0800 Subject: [PATCH 06/10] do not check again for api contract --- .../java/com/ichi2/anki/AnkiDroidJsAPI.kt | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index f987c02ea809..a4dae3c2f35e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -126,7 +126,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { if (isAnkiApiNull(apiName)) { showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) return false - } else if (!getJsApiListMap()[apiName]!!) { + } else if (!mJsApiListMap[apiName]!!) { // see 02-string.xml showDeveloperContact(apiErrorCode) return false @@ -200,10 +200,6 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } } - private fun getJsApiListMap(): HashMap { - return mJsApiListMap - } - fun init(byteArray: ByteArray): ByteArray { var apiStatusJson = "" try { @@ -246,13 +242,13 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { "nextTime3" -> convertToByteArray(cardDataForJsAPI.nextTime3) "nextTime4" -> convertToByteArray(cardDataForJsAPI.nextTime4) "toggleFlag" -> ankiToggleFlag(bytes) - "markCard" -> ankiMarkCard(bytes) + "markCard" -> ankiMarkCard() "buryCard" -> ankiBuryCard() "buryNote" -> ankiBuryNote() "suspendCard" -> ankiSuspendCard() "suspendNote" -> ankiSuspendNote() "setCardDue" -> ankiSetCardDue(bytes) - "resetProgress" -> ankiResetProgress(bytes) + "resetProgress" -> ankiResetProgress() "cardMark" -> convertToByteArray(currentCard.note().hasTag("marked")) "cardFlag" -> convertToByteArray(currentCard.userFlag()) "cardReps" -> convertToByteArray(currentCard.reps) @@ -410,8 +406,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { private suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { val data = checkJsApiContract(byteArray) val daysInt = data.second.toInt() - val apiList = getJsApiListMap() - if (!apiList[AnkiDroidJsAPIConstants.SET_CARD_DUE]!!) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.SET_CARD_DUE]!!) { showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) return@withContext convertToByteArray(false) } @@ -428,10 +423,8 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { convertToByteArray(true) } - private suspend fun ankiResetProgress(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - val apiList = getJsApiListMap() - if (!apiList[AnkiDroidJsAPIConstants.RESET_PROGRESS]!!) { + private suspend fun ankiResetProgress(): ByteArray = withContext(Dispatchers.Main) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.RESET_PROGRESS]!!) { showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) return@withContext convertToByteArray(false) } @@ -442,10 +435,8 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { convertToByteArray(true) } - private suspend fun ankiMarkCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - checkJsApiContract(byteArray) - val apiList = getJsApiListMap() - if (!apiList[AnkiDroidJsAPIConstants.MARK_CARD]!!) { + private suspend fun ankiMarkCard(): ByteArray = withContext(Dispatchers.Main) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.MARK_CARD]!!) { showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) return@withContext convertToByteArray(false) } @@ -457,8 +448,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { private suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { val flag = checkJsApiContract(byteArray).second // flag card (blue, green, orange, red) using javascript from AnkiDroid webview - val apiList = getJsApiListMap() - if (!apiList[AnkiDroidJsAPIConstants.TOGGLE_FLAG]!!) { + if (!mJsApiListMap[AnkiDroidJsAPIConstants.TOGGLE_FLAG]!!) { showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) return@withContext convertToByteArray(false) } From b792bda42ff882ce52a7f99be19c1cd20bc60531 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:50:46 +0800 Subject: [PATCH 07/10] refactor js api, remove redudant check --- AnkiDroid/src/main/assets/scripts/js-api.js | 6 +- .../com/ichi2/anki/AbstractFlashcardViewer.kt | 3 - .../java/com/ichi2/anki/AnkiDroidJsAPI.kt | 328 +++++------------- .../com/ichi2/anki/AnkiDroidJsAPIConstants.kt | 38 +- .../java/com/ichi2/anki/AnkiDroidJsAPITest.kt | 33 -- 5 files changed, 109 insertions(+), 299 deletions(-) diff --git a/AnkiDroid/src/main/assets/scripts/js-api.js b/AnkiDroid/src/main/assets/scripts/js-api.js index b33d3e9ad0be..9ce71251f97a 100644 --- a/AnkiDroid/src/main/assets/scripts/js-api.js +++ b/AnkiDroid/src/main/assets/scripts/js-api.js @@ -66,17 +66,13 @@ class AnkiDroidJS { constructor({ developer, version }) { this.developer = developer; this.version = version; - this.initJSRequest(); + this.handleRequest(`init`); } static init({ developer, version }) { return new AnkiDroidJS({ developer, version }); } - async initJSRequest() { - return await this.handleRequest(`init`); - } - handleRequest = async (endpoint, data) => { if (!this.developer || !this.version) { throw new Error("You must initialize API before using other JS API"); diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index c596e9352996..018df181c2da 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -1312,9 +1312,6 @@ abstract class AbstractFlashcardViewer : open fun displayCardQuestion() { displayCardQuestion(false) - - // js api initialisation / reset - mAnkiDroidJsAPI!!.init() } private fun displayCardQuestion(reload: Boolean) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index a4dae3c2f35e..b52990a29d13 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -25,6 +25,16 @@ import android.net.Uri 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.ankiJsErrorCodeDefault +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 @@ -32,7 +42,6 @@ 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.Decks import com.ichi2.libanki.SortOrder import com.ichi2.utils.NetworkUtils @@ -42,7 +51,6 @@ 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!! @@ -57,9 +65,6 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { private var cardSuppliedApiVersion = "" private var cardSuppliedData = "" - // JS api list enable/disable status - private var mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() - // Text to speech private val mTalker = JavaScriptTTS() @@ -79,14 +84,6 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { return string.toByteArray() } - // init or reset api list - fun init() { - cardSuppliedApiVersion = "" - cardSuppliedDeveloperContact = "" - cardSuppliedData = "" - mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() - } - /** * The method parse json data and check for api version, developer contact * and extract card supplied data if api version and developer contact @@ -95,43 +92,27 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * @return card supplied data, it may be empty, or specific to js api, * in case of tts api it contains json string of text and queueMode which parsed in speak tts api */ - open fun checkJsApiContract(byteArray: ByteArray): Pair { - val data = JSONObject(byteArray.decodeToString()) - cardSuppliedApiVersion = data.optString("version", "") - cardSuppliedDeveloperContact = data.optString("developer", "") - cardSuppliedData = data.optString("data", "") - val isValidVersion = requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact) - if (isValidVersion) { - enableJsApi() - } else { - mJsApiListMap = AnkiDroidJsAPIConstants.initApiMap() - } - return Pair(isValidVersion, cardSuppliedData) - } - - // Check if value null - private fun isAnkiApiNull(api: String): Boolean { - return mJsApiListMap[api] == null - } - - /** - * 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 - */ - private fun isInit(apiName: String, apiErrorCode: Int): Boolean { - if (isAnkiApiNull(apiName)) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) - return false - } else if (!mJsApiListMap[apiName]!!) { - // see 02-string.xml - showDeveloperContact(apiErrorCode) - return false + private fun checkJsApiContract(byteArray: ByteArray): Pair { + try { + val data = JSONObject(byteArray.decodeToString()) + cardSuppliedApiVersion = data.optString("version", "") + cardSuppliedDeveloperContact = data.optString("developer", "") + cardSuppliedData = data.optString("data", "") + val isValidVersion = requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact) + return Pair(isValidVersion, cardSuppliedData) + } catch (j: JSONException) { + Timber.w(j) + activity.runOnUiThread { + activity.showSnackbar( + context.getString( + R.string.invalid_json_data, + j.localizedMessage + ) + ) + } } - return true + showDeveloperContact(ankiJsErrorCodeDefault) + return Pair(false, cardSuppliedData) } /* @@ -193,27 +174,6 @@ 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 - } - } - - fun init(byteArray: ByteArray): ByteArray { - var apiStatusJson = "" - try { - checkJsApiContract(byteArray) - 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)) - } - } - return convertToByteArray(apiStatusJson) - } - /** * Handle js api request, * some of the methods are overriden in Reviewer.kt and default values are returned. @@ -230,9 +190,10 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } val cardDataForJsAPI = activity.getCardDataForJsApi() + val apiParams = data.second return@withContext when (methodName) { - "init" -> init(bytes) + "init" -> convertToByteArray(data.first) "newCardCount" -> convertToByteArray(cardDataForJsAPI.newCardCount) "lrnCardCount" -> convertToByteArray(cardDataForJsAPI.lrnCardCount) "revCardCount" -> convertToByteArray(cardDataForJsAPI.revCardCount) @@ -241,14 +202,37 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { "nextTime2" -> convertToByteArray(cardDataForJsAPI.nextTime2) "nextTime3" -> convertToByteArray(cardDataForJsAPI.nextTime3) "nextTime4" -> convertToByteArray(cardDataForJsAPI.nextTime4) - "toggleFlag" -> ankiToggleFlag(bytes) - "markCard" -> ankiMarkCard() - "buryCard" -> ankiBuryCard() - "buryNote" -> ankiBuryNote() - "suspendCard" -> ankiSuspendCard() - "suspendNote" -> ankiSuspendNote() - "setCardDue" -> ankiSetCardDue(bytes) - "resetProgress" -> ankiResetProgress() + "toggleFlag" -> { + if (apiParams !in flagCommands) { + showDeveloperContact(ankiJsErrorCodeFlagCard) + return@withContext convertToByteArray(false) + } + convertToByteArray(activity.executeCommand(flagCommands[apiParams]!!)) + } + "markCard" -> processAction({ activity.executeCommand(ViewerCommand.MARK) }, ankiJsErrorCodeMarkCard, ::convertToByteArray) + "buryCard" -> processAction(activity::buryCard, ankiJsErrorCodeBuryCard, ::convertToByteArray) + "buryNote" -> processAction(activity::buryNote, ankiJsErrorCodeBuryNote, ::convertToByteArray) + "suspendCard" -> processAction(activity::suspendCard, ankiJsErrorCodeSuspendCard, ::convertToByteArray) + "suspendNote" -> processAction(activity::suspendNote, ankiJsErrorCodeSuspendNote, ::convertToByteArray) + "setCardDue" -> { + try { + val days = apiParams.toInt() + if (days < 0 || days > 9999) { + showDeveloperContact(ankiJsErrorCodeSetDue) + return@withContext convertToByteArray(false) + } + activity.launchCatchingTask { activity.rescheduleCards(listOf(currentCard.id), days) } + return@withContext convertToByteArray(true) + } catch (e: NumberFormatException) { + showDeveloperContact(ankiJsErrorCodeSetDue) + return@withContext convertToByteArray(false) + } + } + "resetProgress" -> { + val cardIds = listOf(currentCard.id) + activity.launchCatchingTask { activity.resetCards(cardIds) } + convertToByteArray(true) + } "cardMark" -> convertToByteArray(currentCard.note().hasTag("marked")) "cardFlag" -> convertToByteArray(currentCard.userFlag()) "cardReps" -> convertToByteArray(currentCard.reps) @@ -267,18 +251,31 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { "cardDue" -> convertToByteArray(currentCard.due) "deckName" -> convertToByteArray(Decks.basename(activity.getColUnsafe.decks.name(currentCard.did))) "isActiveNetworkMetered" -> convertToByteArray(NetworkUtils.isActiveNetworkMetered()) - "ttsSetLanguage" -> convertToByteArray(mTalker.setLanguage(data.second)) - "ttsSpeak" -> ankiTtsSpeak(bytes) + "ttsSetLanguage" -> convertToByteArray(mTalker.setLanguage(apiParams)) + "ttsSpeak" -> { + val jsonObject = JSONObject(apiParams) + val text = jsonObject.getString("text") + val queueMode = jsonObject.getInt("queueMode") + convertToByteArray(mTalker.speak(text, queueMode)) + } "ttsIsSpeaking" -> convertToByteArray(mTalker.isSpeaking) - "ttsSetPitch" -> convertToByteArray(mTalker.setPitch(data.second.toFloat())) - "ttsSetSpeechRate" -> convertToByteArray(mTalker.setSpeechRate(data.second.toFloat())) - "ttsFieldModifierIsAvailable" -> + "ttsSetPitch" -> convertToByteArray(mTalker.setPitch(apiParams.toFloat())) + "ttsSetSpeechRate" -> convertToByteArray(mTalker.setSpeechRate(apiParams.toFloat())) + "ttsFieldModifierIsAvailable" -> { // Know if {{tts}} is supported - issue #10443 // Return false for now convertToByteArray(false) + } "ttsStop" -> convertToByteArray(mTalker.stop()) - "searchCard" -> ankiSearchCard(bytes) - "searchCardWithCallback" -> ankiSearchCardWithCallback(bytes) + "searchCard" -> { + val intent = Intent(context, CardBrowser::class.java).apply { + putExtra("currentCard", currentCard.id) + putExtra("search_query", apiParams) + } + activity.startActivityWithAnimation(intent, ActivityTransitionAnimation.Direction.START) + convertToByteArray(true) + } + "searchCardWithCallback" -> ankiSearchCardWithCallback(apiParams) "isDisplayingAnswer" -> convertToByteArray(activity.isDisplayingAnswer) "addTagToCard" -> { activity.runOnUiThread { activity.showTagsDialog() } @@ -288,91 +285,37 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { "isTopbarShown" -> convertToByteArray(activity.prefShowTopbar) "isInNightMode" -> convertToByteArray(activity.isInNightMode) "enableHorizontalScrollbar" -> { - activity.webView!!.isHorizontalScrollBarEnabled = data.second.toBoolean() + activity.webView!!.isHorizontalScrollBarEnabled = apiParams.toBoolean() convertToByteArray(true) } "enableVerticalScrollbar" -> { - activity.webView!!.isVerticalScrollBarEnabled = data.second.toBoolean() + activity.webView!!.isVerticalScrollBarEnabled = apiParams.toBoolean() convertToByteArray(true) } else -> { + showDeveloperContact(ankiJsErrorCodeError) throw Exception("unhandled request: $methodName") } } } - private suspend fun ankiBuryCard(): ByteArray = withContext(Dispatchers.Main) { - if (!mJsApiListMap[AnkiDroidJsAPIConstants.BURY_CARD]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryCard) - return@withContext convertToByteArray(false) + private fun processAction(action: () -> Boolean, errorCode: Int, conversion: (Boolean) -> ByteArray): ByteArray { + val status = action() + if (!status) { + showDeveloperContact(errorCode) } - - convertToByteArray(activity.buryCard()) + return conversion(status) } - private suspend fun ankiBuryNote(): ByteArray = withContext(Dispatchers.Main) { - if (!mJsApiListMap[AnkiDroidJsAPIConstants.BURY_NOTE]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeBuryNote) - return@withContext convertToByteArray(false) - } - - convertToByteArray(activity.buryNote()) - } - - private suspend fun ankiSuspendCard(): ByteArray = withContext(Dispatchers.Main) { - if (!mJsApiListMap[AnkiDroidJsAPIConstants.SUSPEND_CARD]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendCard) - return@withContext convertToByteArray(false) - } - - convertToByteArray(activity.suspendCard()) - } - - private suspend fun ankiSuspendNote(): ByteArray = withContext(Dispatchers.Main) { - if (!mJsApiListMap[AnkiDroidJsAPIConstants.SUSPEND_NOTE]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSuspendNote) - return@withContext convertToByteArray(false) - } - - convertToByteArray(activity.suspendNote()) - } - - private suspend fun ankiSearchCard(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } - val intent = Intent(context, CardBrowser::class.java) - val currentCardId: CardId = currentCard.id - intent.putExtra("currentCard", currentCardId) - intent.putExtra("search_query", data.second) - activity.startActivityWithAnimation(intent, ActivityTransitionAnimation.Direction.START) - convertToByteArray(true) - } - - private suspend fun ankiTtsSpeak(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(-1) - } - val jsonObject = JSONObject(data.second) - val text = jsonObject.getString("text") - val queueMode = jsonObject.getInt("queueMode") - convertToByteArray(mTalker.speak(text, queueMode)) - } - - private suspend fun ankiSearchCardWithCallback(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - if (!data.first) { - return@withContext convertToByteArray(false) - } + private suspend fun ankiSearchCardWithCallback(query: String): ByteArray = withContext(Dispatchers.Main) { val cards = try { - searchForCards(data.second, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) + searchForCards(query, SortOrder.UseCollectionOrdering(), CardsOrNotes.CARDS) } catch (exc: Exception) { activity.webView!!.evaluateJavascript( "console.log('${context.getString(R.string.search_card_js_api_no_results)}')", null ) + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSearchCard) return@withContext convertToByteArray(false) } val searchResult: MutableList = ArrayList() @@ -403,89 +346,6 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { convertToByteArray(true) } - private suspend fun ankiSetCardDue(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val data = checkJsApiContract(byteArray) - val daysInt = data.second.toInt() - if (!mJsApiListMap[AnkiDroidJsAPIConstants.SET_CARD_DUE]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - - if (daysInt < 0 || daysInt > 9999) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSetDue) - convertToByteArray(false) - } - - val cardIds = listOf(currentCard.id) - activity.launchCatchingTask { - activity.rescheduleCards(cardIds, daysInt) - } - convertToByteArray(true) - } - - private suspend fun ankiResetProgress(): ByteArray = withContext(Dispatchers.Main) { - if (!mJsApiListMap[AnkiDroidJsAPIConstants.RESET_PROGRESS]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - val cardIds = listOf(currentCard.id) - activity.launchCatchingTask { - activity.resetCards(cardIds) - } - convertToByteArray(true) - } - - private suspend fun ankiMarkCard(): ByteArray = withContext(Dispatchers.Main) { - if (!mJsApiListMap[AnkiDroidJsAPIConstants.MARK_CARD]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - - activity.executeCommand(ViewerCommand.MARK) - convertToByteArray(true) - } - - private suspend fun ankiToggleFlag(byteArray: ByteArray): ByteArray = withContext(Dispatchers.Main) { - val flag = checkJsApiContract(byteArray).second - // flag card (blue, green, orange, red) using javascript from AnkiDroid webview - if (!mJsApiListMap[AnkiDroidJsAPIConstants.TOGGLE_FLAG]!!) { - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeDefault) - return@withContext convertToByteArray(false) - } - - when (flag) { - "none" -> { - activity.executeCommand(ViewerCommand.UNSET_FLAG) - } - "red" -> { - activity.executeCommand(ViewerCommand.TOGGLE_FLAG_RED) - } - "orange" -> { - activity.executeCommand(ViewerCommand.TOGGLE_FLAG_ORANGE) - } - "green" -> { - activity.executeCommand(ViewerCommand.TOGGLE_FLAG_GREEN) - } - "blue" -> { - activity.executeCommand(ViewerCommand.TOGGLE_FLAG_BLUE) - } - "pink" -> { - activity.executeCommand(ViewerCommand.TOGGLE_FLAG_PINK) - } - "turquoise" -> { - activity.executeCommand(ViewerCommand.TOGGLE_FLAG_TURQUOISE) - } - "purple" -> { - activity.executeCommand(ViewerCommand.TOGGLE_FLAG_PURPLE) - } - else -> { - Timber.d("No such Flag found.") - convertToByteArray(false) - } - } - convertToByteArray(true) - } - open class CardDataForJsApi { var newCardCount = "" var lrnCardCount = "" diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPIConstants.kt index 138b09a761a3..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.2" const val sMinimumJsApiVersion = "0.0.2" - 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 - } + 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/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt index 8ff73d77f26e..67c54bcd3ddc 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt @@ -34,32 +34,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AnkiDroidJsAPITest : RobolectricTest() { - @Test - fun initTest() = runTest { - 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() - - // 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.handleJsApiRequest("init", jsApiContract(), true) - .decodeToString(), - equalTo(expected) - ) - } - @Test fun ankiGetNextTimeTest() = runTest { val models = col.notetypes @@ -270,9 +244,6 @@ class AnkiDroidJsAPITest : RobolectricTest() { val reviewer: Reviewer = startReviewer() val jsapi = reviewer.javaScriptFunction() - // init js api - jsapi.init(jsApiContract()) - waitForAsyncTasksToComplete() // ---------- // Bury Card @@ -332,8 +303,6 @@ class AnkiDroidJsAPITest : RobolectricTest() { waitForAsyncTasksToComplete() val jsapi = reviewer.javaScriptFunction() - // init js api - jsapi.init(jsApiContract()) // get card id for testing due val cardId = getDataFromRequest("cardId", jsapi).toLong() @@ -372,8 +341,6 @@ class AnkiDroidJsAPITest : RobolectricTest() { waitForAsyncTasksToComplete() val jsapi = reviewer.javaScriptFunction() - // init js api - jsapi.init(jsApiContract()) // get card id for testing due val cardId = getDataFromRequest("cardId", jsapi).toLong() From 78fe106062c4d4bdb46aad99b090d3cf4cb4fe37 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Wed, 6 Dec 2023 22:28:28 +0800 Subject: [PATCH 08/10] refactor js api, return result in json format (success, value) --- AnkiDroid/src/main/assets/scripts/js-api.js | 10 ------ .../java/com/ichi2/anki/AnkiDroidJsAPI.kt | 34 ++++++++++++------- .../src/main/java/com/ichi2/anki/Reviewer.kt | 19 ++++++----- .../java/com/ichi2/anki/AnkiDroidJsAPITest.kt | 10 +++--- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/AnkiDroid/src/main/assets/scripts/js-api.js b/AnkiDroid/src/main/assets/scripts/js-api.js index 9ce71251f97a..780858cee55b 100644 --- a/AnkiDroid/src/main/assets/scripts/js-api.js +++ b/AnkiDroid/src/main/assets/scripts/js-api.js @@ -74,10 +74,6 @@ class AnkiDroidJS { } handleRequest = async (endpoint, data) => { - if (!this.developer || !this.version) { - throw new Error("You must initialize API before using other JS API"); - } - const url = `/jsapi/${endpoint}`; try { const response = await fetch(url, { @@ -111,9 +107,6 @@ class AnkiDroidJS { Object.keys(jsApiList).forEach(method => { if (method === "ankiTtsSpeak") { AnkiDroidJS.prototype[method] = async function (text, queueMode = 0) { - if (this.version < "0.0.2") { - throw new Error("You must update AnkiDroid JS API version."); - } const endpoint = jsApiList[method]; const data = JSON.stringify({ text, queueMode }); return await this.handleRequest(endpoint, data); @@ -121,9 +114,6 @@ Object.keys(jsApiList).forEach(method => { return; } AnkiDroidJS.prototype[method] = async function (data) { - if (this.version < "0.0.2") { - throw new Error("You must update AnkiDroid JS API version."); - } const endpoint = jsApiList[method]; return await this.handleRequest(endpoint, data); }; diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index b52990a29d13..a3357f46f100 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -64,24 +64,25 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { private var cardSuppliedDeveloperContact = "" private var cardSuppliedApiVersion = "" private var cardSuppliedData = "" + private var isValidVersion = false // Text to speech private val mTalker = JavaScriptTTS() open fun convertToByteArray(boolean: Boolean): ByteArray { - return boolean.toString().toByteArray() + return ApiResult(isValidVersion, boolean.toString()).toString().toByteArray() } open fun convertToByteArray(int: Int): ByteArray { - return int.toString().toByteArray() + return ApiResult(isValidVersion, int.toString()).toString().toByteArray() } open fun convertToByteArray(long: Long): ByteArray { - return long.toString().toByteArray() + return ApiResult(isValidVersion, long.toString()).toString().toByteArray() } open fun convertToByteArray(string: String): ByteArray { - return string.toByteArray() + return ApiResult(isValidVersion, string).toString().toByteArray() } /** @@ -92,14 +93,14 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * @return card supplied data, it may be empty, or specific to js api, * in case of tts api it contains json string of text and queueMode which parsed in speak tts api */ - private fun checkJsApiContract(byteArray: ByteArray): Pair { + private fun checkJsApiContract(byteArray: ByteArray) { try { val data = JSONObject(byteArray.decodeToString()) cardSuppliedApiVersion = data.optString("version", "") cardSuppliedDeveloperContact = data.optString("developer", "") cardSuppliedData = data.optString("data", "") - val isValidVersion = requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact) - return Pair(isValidVersion, cardSuppliedData) + isValidVersion = requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact) + return } catch (j: JSONException) { Timber.w(j) activity.runOnUiThread { @@ -112,7 +113,6 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { } } showDeveloperContact(ankiJsErrorCodeDefault) - return Pair(false, cardSuppliedData) } /* @@ -182,18 +182,19 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * @return */ open suspend fun handleJsApiRequest(methodName: String, bytes: ByteArray, isReviewer: Boolean) = withContext(Dispatchers.Main) { - val data = checkJsApiContract(bytes) + // the method will call to set the card supplied data and is valid version for each api request + checkJsApiContract(bytes) // if api not init or is api not called from reviewer then return default -1 // also other action will not be modified - if (!data.first or !isReviewer) { + if (!isValidVersion or !isReviewer) { return@withContext convertToByteArray(-1) } val cardDataForJsAPI = activity.getCardDataForJsApi() - val apiParams = data.second + val apiParams = cardSuppliedData return@withContext when (methodName) { - "init" -> convertToByteArray(data.first) + "init" -> convertToByteArray(isValidVersion) "newCardCount" -> convertToByteArray(cardDataForJsAPI.newCardCount) "lrnCardCount" -> convertToByteArray(cardDataForJsAPI.lrnCardCount) "revCardCount" -> convertToByteArray(cardDataForJsAPI.revCardCount) @@ -356,4 +357,13 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { 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() + } + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 0264e4de3e88..246763190dff 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -1557,15 +1557,16 @@ open class Reviewer : } override fun getCardDataForJsApi(): AnkiDroidJsAPI.CardDataForJsApi { - val cardDataForJsAPI = AnkiDroidJsAPI.CardDataForJsApi() - cardDataForJsAPI.newCardCount = mNewCount.toString() - cardDataForJsAPI.lrnCardCount = mLrnCount.toString() - cardDataForJsAPI.revCardCount = mRevCount.toString() - cardDataForJsAPI.nextTime1 = easeButton1!!.nextTime - cardDataForJsAPI.nextTime2 = easeButton2!!.nextTime - cardDataForJsAPI.nextTime3 = easeButton3!!.nextTime - cardDataForJsAPI.nextTime4 = easeButton4!!.nextTime - cardDataForJsAPI.eta = mEta + 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 } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt index 67c54bcd3ddc..48100697ccdb 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt @@ -363,11 +363,11 @@ class AnkiDroidJsAPITest : RobolectricTest() { companion object { fun jsApiContract(data: String = ""): ByteArray { - val jsonObject = JSONObject() - jsonObject.put("version", "0.0.2") - jsonObject.put("developer", "test@example.com") - jsonObject.put("data", data) - return jsonObject.toString().toByteArray() + return JSONObject().apply { + put("version", "0.0.2") + put("developer", "test@example.com") + put("data", data) + }.toString().toByteArray() } } } From a8baf8e76738a6096ebbceaad244fc96a5cef2b5 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:06:11 +0800 Subject: [PATCH 09/10] update js unit test --- .../java/com/ichi2/anki/AnkiDroidJsAPITest.kt | 220 ++++++++++++------ .../test/java/com/ichi2/anki/ReviewerTest.kt | 21 +- 2 files changed, 159 insertions(+), 82 deletions(-) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt index 48100697ccdb..48488b530246 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt @@ -26,8 +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.assertFalse -import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -53,19 +52,19 @@ class AnkiDroidJsAPITest : RobolectricTest() { assertThat( getDataFromRequest("nextTime1", jsapi).withoutUnicodeIsolation(), - equalTo("<1m") + equalTo(formatApiResult("<1m")) ) assertThat( getDataFromRequest("nextTime2", jsapi).withoutUnicodeIsolation(), - equalTo("<6m") + equalTo(formatApiResult("<6m")) ) assertThat( getDataFromRequest("nextTime3", jsapi).withoutUnicodeIsolation(), - equalTo("<10m") + equalTo(formatApiResult("<10m")) ) assertThat( getDataFromRequest("nextTime4", jsapi).withoutUnicodeIsolation(), - equalTo("4d") + equalTo(formatApiResult("4d")) ) } @@ -88,43 +87,97 @@ class AnkiDroidJsAPITest : RobolectricTest() { val currentCard = reviewer.currentCard!! // Card Did - assertThat(getDataFromRequest("cardDid", jsapi).toLong(), equalTo(currentCard.did)) + assertThat( + getDataFromRequest("cardDid", jsapi), + equalTo(formatApiResult(currentCard.did)) + ) // Card Id - assertThat(getDataFromRequest("cardId", jsapi).toLong(), equalTo(currentCard.id)) + assertThat( + getDataFromRequest("cardId", jsapi), + equalTo(formatApiResult(currentCard.id)) + ) // Card Nid - assertThat(getDataFromRequest("cardNid", jsapi).toLong(), equalTo(currentCard.nid)) + assertThat( + getDataFromRequest("cardNid", jsapi), + equalTo(formatApiResult(currentCard.nid)) + ) // Card ODid - assertThat(getDataFromRequest("cardODid", jsapi).toLong(), equalTo(currentCard.oDid)) + assertThat( + getDataFromRequest("cardODid", jsapi), + equalTo(formatApiResult(currentCard.oDid)) + ) // Card Type - assertThat(getDataFromRequest("cardType", jsapi).toInt(), equalTo(currentCard.type)) + assertThat( + getDataFromRequest("cardType", jsapi), + equalTo(formatApiResult(currentCard.type)) + ) // Card ODue - assertThat(getDataFromRequest("cardODue", jsapi).toLong(), equalTo(currentCard.oDue)) + assertThat( + getDataFromRequest("cardODue", jsapi), + equalTo(formatApiResult(currentCard.oDue)) + ) // Card Due - assertThat(getDataFromRequest("cardDue", jsapi).toLong(), equalTo(currentCard.due)) + assertThat( + getDataFromRequest("cardDue", jsapi), + equalTo(formatApiResult(currentCard.due)) + ) // Card Factor - assertThat(getDataFromRequest("cardFactor", jsapi).toInt(), equalTo(currentCard.factor)) + assertThat( + getDataFromRequest("cardFactor", jsapi), + equalTo(formatApiResult(currentCard.factor)) + ) // Card Lapses - assertThat(getDataFromRequest("cardLapses", jsapi).toInt(), equalTo(currentCard.lapses)) + assertThat( + getDataFromRequest("cardLapses", jsapi), + equalTo(formatApiResult(currentCard.lapses)) + ) // Card Ivl - assertThat(getDataFromRequest("cardInterval", jsapi).toInt(), equalTo(currentCard.ivl)) + assertThat( + getDataFromRequest("cardInterval", jsapi), + equalTo(formatApiResult(currentCard.ivl)) + ) // Card mod - assertThat(getDataFromRequest("cardMod", jsapi).toLong(), equalTo(currentCard.mod)) + assertThat( + getDataFromRequest("cardMod", jsapi), + equalTo(formatApiResult(currentCard.mod)) + ) // Card Queue - assertThat(getDataFromRequest("cardQueue", jsapi).toInt(), equalTo(currentCard.queue)) + assertThat( + getDataFromRequest("cardQueue", jsapi), + equalTo(formatApiResult(currentCard.queue)) + ) // Card Reps - assertThat(getDataFromRequest("cardReps", jsapi).toInt(), equalTo(currentCard.reps)) + assertThat( + getDataFromRequest("cardReps", jsapi), + equalTo(formatApiResult(currentCard.reps)) + ) // Card left - assertThat(getDataFromRequest("cardLeft", jsapi).toInt(), equalTo(currentCard.left)) + assertThat( + getDataFromRequest("cardLeft", jsapi), + equalTo(formatApiResult(currentCard.left)) + ) // Card Flag - assertThat(getDataFromRequest("cardFlag", jsapi).toInt(), equalTo(0)) + assertThat( + getDataFromRequest("cardFlag", jsapi), + equalTo(formatApiResult(0)) + ) reviewer.currentCard!!.setFlag(1) - assertThat(getDataFromRequest("cardFlag", jsapi).toInt(), equalTo(1)) + assertThat( + getDataFromRequest("cardFlag", jsapi), + equalTo(formatApiResult(1)) + ) // Card Mark - assertThat(getDataFromRequest("cardMark", jsapi).toBoolean(), equalTo(false)) + assertThat( + getDataFromRequest("cardMark", jsapi), + equalTo(formatApiResult(false)) + ) reviewer.currentCard!!.note().addTag("marked") - assertThat(getDataFromRequest("cardMark", jsapi).toBoolean(), equalTo(true)) + assertThat( + getDataFromRequest("cardMark", jsapi), + equalTo(formatApiResult(true)) + ) } @Test @@ -144,29 +197,29 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Displaying question assertThat( - getDataFromRequest("isDisplayingAnswer", jsapi).toBoolean(), - equalTo(reviewer.isDisplayingAnswer) + getDataFromRequest("isDisplayingAnswer", jsapi), + equalTo(formatApiResult(reviewer.isDisplayingAnswer)) ) reviewer.displayCardAnswer() assertThat( - getDataFromRequest("isDisplayingAnswer", jsapi).toBoolean(), - equalTo(reviewer.isDisplayingAnswer) + getDataFromRequest("isDisplayingAnswer", jsapi), + equalTo(formatApiResult(reviewer.isDisplayingAnswer)) ) // Full Screen assertThat( - getDataFromRequest("isInFullscreen", jsapi).toBoolean(), - equalTo(reviewer.isFullscreen) + getDataFromRequest("isInFullscreen", jsapi), + equalTo(formatApiResult(reviewer.isFullscreen)) ) // Top bar assertThat( - getDataFromRequest("isTopbarShown", jsapi).toBoolean(), - equalTo(reviewer.prefShowTopbar) + getDataFromRequest("isTopbarShown", jsapi), + equalTo(formatApiResult(reviewer.prefShowTopbar)) ) // Night Mode assertThat( - getDataFromRequest("isInNightMode", jsapi).toBoolean(), - equalTo(reviewer.isInNightMode) + getDataFromRequest("isInNightMode", jsapi), + equalTo(formatApiResult(reviewer.isInNightMode)) ) } @@ -190,41 +243,46 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Card mark test // --------------- // Before marking card - assertFalse(getDataFromRequest("cardMark", jsapi).toBoolean()) + assertThat( + getDataFromRequest("cardMark", jsapi), + equalTo(formatApiResult(false)) + ) // Mark card - assertTrue( - jsapi.handleJsApiRequest("markCard", jsApiContract("true"), true) - .decodeToString().toBoolean() + assertThat( + getDataFromRequest("markCard", jsapi, "true"), + equalTo(formatApiResult(true)) ) + // After marking card - assertTrue(getDataFromRequest("cardMark", jsapi).toBoolean()) + assertThat( + getDataFromRequest("cardMark", jsapi), + equalTo(formatApiResult(true)) + ) // --------------- // Card flag test // --------------- // before toggling flag assertThat( - jsapi.handleJsApiRequest("cardFlag", jsApiContract(), true) - .decodeToString().toInt(), - equalTo(0) + getDataFromRequest("cardFlag", jsapi), + equalTo(formatApiResult(0)) ) // call javascript function to toggle flag assertThat( - jsapi.handleJsApiRequest("toggleFlag", jsApiContract("red"), true) - .decodeToString().toBoolean(), - equalTo(true) + getDataFromRequest("toggleFlag", jsapi, "red"), + equalTo(formatApiResult(true)) ) + // after toggling flag assertThat( - jsapi.handleJsApiRequest("cardFlag", jsApiContract(), true) - .decodeToString().toInt(), - equalTo(1) + getDataFromRequest("cardFlag", jsapi), + equalTo(formatApiResult(1)) ) } - // TODO - update test + @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 @@ -249,7 +307,10 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Bury Card // ---------- // call script to bury current card - assertTrue(getDataFromRequest("buryCard", jsapi).toBoolean()) + assertThat( + getDataFromRequest("buryCard", jsapi), + equalTo(formatApiResult(true)) + ) // count number of notes val sched = reviewer.getColUnsafe @@ -259,7 +320,10 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Bury Note // ---------- // call script to bury current note - assertTrue(getDataFromRequest("buryNote", jsapi).toBoolean()) + assertThat( + getDataFromRequest("buryNote", jsapi), + equalTo(formatApiResult(true)) + ) // count number of notes assertThat(sched.cardCount(), equalTo(3)) @@ -268,7 +332,10 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Suspend Card // ------------- // call script to suspend current card - assertTrue(getDataFromRequest("suspendCard", jsapi).toBoolean()) + assertThat( + getDataFromRequest("suspendCard", jsapi), + equalTo(formatApiResult(true)) + ) // count number of notes assertThat(sched.cardCount(), equalTo(2)) @@ -277,7 +344,10 @@ class AnkiDroidJsAPITest : RobolectricTest() { // Suspend Note // ------------- // call script to suspend current note - assertTrue(getDataFromRequest("suspendNote", jsapi).toBoolean()) + assertThat( + getDataFromRequest("suspendNote", jsapi), + equalTo(formatApiResult(true)) + ) // count number of notes assertThat(sched.cardCount(), equalTo(1)) @@ -304,20 +374,18 @@ class AnkiDroidJsAPITest : RobolectricTest() { val jsapi = reviewer.javaScriptFunction() // get card id for testing due - val cardId = getDataFromRequest("cardId", jsapi).toLong() + 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", - jsapi.handleJsApiRequest("setCardDue", jsApiContract("15"), true) - .decodeToString().toBoolean() - ) + 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) + val cardToBeReschedule = col.getCard(cardId) + assertEquals("Card is rescheduled", 15L + col.sched.today, cardToBeReschedule.due) } @Test @@ -341,26 +409,19 @@ class AnkiDroidJsAPITest : RobolectricTest() { waitForAsyncTasksToComplete() val jsapi = reviewer.javaScriptFunction() - // get card id for testing due - val cardId = getDataFromRequest("cardId", jsapi).toLong() // test that card reset - assertTrue("Card progress reset", getDataFromRequest("resetProgress", jsapi).toBoolean()) + 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) } - private suspend fun getDataFromRequest(methodName: String, jsAPI: AnkiDroidJsAPI): String { - return jsAPI.handleJsApiRequest(methodName, jsApiContract(), true) - .decodeToString() - } - companion object { fun jsApiContract(data: String = ""): ByteArray { return JSONObject().apply { @@ -369,5 +430,24 @@ class AnkiDroidJsAPITest : RobolectricTest() { put("data", data) }.toString().toByteArray() } + + fun formatApiResult(res: Any): String { + return when (res) { + is String -> "{\"success\":true,\"value\":\"$res\"}" + is Boolean -> "{\"success\":true,\"value\":\"$res\"}" + is Int -> "{\"success\":true,\"value\":\"$res\"}" + is Long -> "{\"success\":true,\"value\":\"$res\"}" + else -> "{\"success\":true,\"value\":-1}" + } + } + + 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 cf699daea142..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,8 @@ 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 @@ -242,7 +244,7 @@ class ReviewerTest : RobolectricTest() { assertThat( javaScriptFunction.handleJsApiRequest("deckName", jsApiContract(), true) .decodeToString(), - equalTo("B") + equalTo(formatApiResult("B")) ) } @@ -332,20 +334,15 @@ class ReviewerTest : RobolectricTest() { private fun assertCounts(r: Reviewer, newCount: Int, stepCount: Int, revCount: Int) = runTest { val jsApi = r.javaScriptFunction() val countList = listOf( - jsApi.handleJsApiRequest("newCardCount", jsApiContract(), true) - .decodeToString().toInt(), - jsApi.handleJsApiRequest("lrnCardCount", jsApiContract(), true) - .decodeToString().toInt(), - jsApi.handleJsApiRequest("revCardCount", jsApiContract(), true) - .decodeToString().toInt() + 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()) From 89167ceccde5839e459bbcd10a03d87727bd5e49 Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:29:17 +0800 Subject: [PATCH 10/10] parse api contract to api data class --- .../java/com/ichi2/anki/AnkiDroidJsAPI.kt | 214 +++++++++--------- .../java/com/ichi2/anki/AnkiDroidJsAPITest.kt | 8 +- 2 files changed, 108 insertions(+), 114 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt index a3357f46f100..6227d7840357 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidJsAPI.kt @@ -27,7 +27,6 @@ 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.ankiJsErrorCodeDefault import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeError import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeFlagCard import com.ichi2.anki.AnkiDroidJsAPIConstants.ankiJsErrorCodeMarkCard @@ -61,58 +60,46 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { */ private val context: Context = activity - private var cardSuppliedDeveloperContact = "" - private var cardSuppliedApiVersion = "" - private var cardSuppliedData = "" - private var isValidVersion = false // Text to speech private val mTalker = JavaScriptTTS() - open fun convertToByteArray(boolean: Boolean): ByteArray { - return ApiResult(isValidVersion, boolean.toString()).toString().toByteArray() + open fun convertToByteArray(apiContract: ApiContract, boolean: Boolean): ByteArray { + return ApiResult(apiContract.isValid, boolean.toString()).toString().toByteArray() } - open fun convertToByteArray(int: Int): ByteArray { - return ApiResult(isValidVersion, int.toString()).toString().toByteArray() + open fun convertToByteArray(apiContract: ApiContract, int: Int): ByteArray { + return ApiResult(apiContract.isValid, int.toString()).toString().toByteArray() } - open fun convertToByteArray(long: Long): ByteArray { - return ApiResult(isValidVersion, long.toString()).toString().toByteArray() + open fun convertToByteArray(apiContract: ApiContract, long: Long): ByteArray { + return ApiResult(apiContract.isValid, long.toString()).toString().toByteArray() } - open fun convertToByteArray(string: String): ByteArray { - return ApiResult(isValidVersion, string).toString().toByteArray() + open fun convertToByteArray(apiContract: ApiContract, string: String): ByteArray { + return ApiResult(apiContract.isValid, string).toString().toByteArray() } /** - * The method parse json data and check for api version, developer contact - * and extract card supplied data if api version and developer contact - * provided then enable js api otherwise disable js api. + * The method parse json data and return api contract object * @param byteArray - * @return card supplied data, it may be empty, or specific to js api, - * in case of tts api it contains json string of text and queueMode which parsed in speak tts api + * @return apiContract or null */ - private fun checkJsApiContract(byteArray: ByteArray) { + private fun parseJsApiContract(byteArray: ByteArray): ApiContract? { try { val data = JSONObject(byteArray.decodeToString()) - cardSuppliedApiVersion = data.optString("version", "") - cardSuppliedDeveloperContact = data.optString("developer", "") - cardSuppliedData = data.optString("data", "") - isValidVersion = requireApiVersion(cardSuppliedApiVersion, cardSuppliedDeveloperContact) - return + 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 - ) - ) + activity.showSnackbar(context.getString(R.string.invalid_json_data, j.localizedMessage)) } } - showDeveloperContact(ankiJsErrorCodeDefault) + return null } /* @@ -123,9 +110,9 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * * show developer contact if js api used in card is deprecated */ - private 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) @@ -140,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) @@ -157,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 } @@ -179,145 +169,153 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { * 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 - checkJsApiContract(bytes) + 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 (!isValidVersion or !isReviewer) { - return@withContext convertToByteArray(-1) + if (!apiContract.isValid or !isReviewer) { + return@withContext convertToByteArray(apiContract, -1) } val cardDataForJsAPI = activity.getCardDataForJsApi() - val apiParams = cardSuppliedData + val apiParams = apiContract.cardSuppliedData return@withContext when (methodName) { - "init" -> convertToByteArray(isValidVersion) - "newCardCount" -> convertToByteArray(cardDataForJsAPI.newCardCount) - "lrnCardCount" -> convertToByteArray(cardDataForJsAPI.lrnCardCount) - "revCardCount" -> convertToByteArray(cardDataForJsAPI.revCardCount) - "eta" -> convertToByteArray(cardDataForJsAPI.eta) - "nextTime1" -> convertToByteArray(cardDataForJsAPI.nextTime1) - "nextTime2" -> convertToByteArray(cardDataForJsAPI.nextTime2) - "nextTime3" -> convertToByteArray(cardDataForJsAPI.nextTime3) - "nextTime4" -> convertToByteArray(cardDataForJsAPI.nextTime4) + "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) - return@withContext convertToByteArray(false) + showDeveloperContact(ankiJsErrorCodeFlagCard, apiContract.cardSuppliedDeveloperContact) + return@withContext convertToByteArray(apiContract, false) } - convertToByteArray(activity.executeCommand(flagCommands[apiParams]!!)) + convertToByteArray(apiContract, activity.executeCommand(flagCommands[apiParams]!!)) } - "markCard" -> processAction({ activity.executeCommand(ViewerCommand.MARK) }, ankiJsErrorCodeMarkCard, ::convertToByteArray) - "buryCard" -> processAction(activity::buryCard, ankiJsErrorCodeBuryCard, ::convertToByteArray) - "buryNote" -> processAction(activity::buryNote, ankiJsErrorCodeBuryNote, ::convertToByteArray) - "suspendCard" -> processAction(activity::suspendCard, ankiJsErrorCodeSuspendCard, ::convertToByteArray) - "suspendNote" -> processAction(activity::suspendNote, ankiJsErrorCodeSuspendNote, ::convertToByteArray) + "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) - return@withContext convertToByteArray(false) + showDeveloperContact(ankiJsErrorCodeSetDue, apiContract.cardSuppliedDeveloperContact) + return@withContext convertToByteArray(apiContract, false) + } + activity.launchCatchingTask { + activity.rescheduleCards(listOf(currentCard.id), days) } - activity.launchCatchingTask { activity.rescheduleCards(listOf(currentCard.id), days) } - return@withContext convertToByteArray(true) + return@withContext convertToByteArray(apiContract, true) } catch (e: NumberFormatException) { - showDeveloperContact(ankiJsErrorCodeSetDue) - return@withContext convertToByteArray(false) + showDeveloperContact(ankiJsErrorCodeSetDue, apiContract.cardSuppliedDeveloperContact) + return@withContext convertToByteArray(apiContract, false) } } "resetProgress" -> { val cardIds = listOf(currentCard.id) activity.launchCatchingTask { activity.resetCards(cardIds) } - convertToByteArray(true) + convertToByteArray(apiContract, true) } - "cardMark" -> convertToByteArray(currentCard.note().hasTag("marked")) - "cardFlag" -> convertToByteArray(currentCard.userFlag()) - "cardReps" -> convertToByteArray(currentCard.reps) - "cardInterval" -> convertToByteArray(currentCard.ivl) - "cardFactor" -> convertToByteArray(currentCard.factor) - "cardMod" -> convertToByteArray(currentCard.mod) - "cardId" -> convertToByteArray(currentCard.id) - "cardNid" -> convertToByteArray(currentCard.nid) - "cardType" -> convertToByteArray(currentCard.type) - "cardDid" -> convertToByteArray(currentCard.did) - "cardLeft" -> convertToByteArray(currentCard.left) - "cardODid" -> convertToByteArray(currentCard.oDid) - "cardODue" -> convertToByteArray(currentCard.oDue) - "cardQueue" -> convertToByteArray(currentCard.queue) - "cardLapses" -> convertToByteArray(currentCard.lapses) - "cardDue" -> convertToByteArray(currentCard.due) - "deckName" -> convertToByteArray(Decks.basename(activity.getColUnsafe.decks.name(currentCard.did))) - "isActiveNetworkMetered" -> convertToByteArray(NetworkUtils.isActiveNetworkMetered()) - "ttsSetLanguage" -> convertToByteArray(mTalker.setLanguage(apiParams)) + "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(mTalker.speak(text, queueMode)) + convertToByteArray(apiContract, mTalker.speak(text, queueMode)) } - "ttsIsSpeaking" -> convertToByteArray(mTalker.isSpeaking) - "ttsSetPitch" -> convertToByteArray(mTalker.setPitch(apiParams.toFloat())) - "ttsSetSpeechRate" -> convertToByteArray(mTalker.setSpeechRate(apiParams.toFloat())) + "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(false) + convertToByteArray(apiContract, false) } - "ttsStop" -> convertToByteArray(mTalker.stop()) + "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(true) + convertToByteArray(apiContract, true) } - "searchCardWithCallback" -> ankiSearchCardWithCallback(apiParams) - "isDisplayingAnswer" -> convertToByteArray(activity.isDisplayingAnswer) + "searchCardWithCallback" -> ankiSearchCardWithCallback(apiContract) + "isDisplayingAnswer" -> convertToByteArray(apiContract, activity.isDisplayingAnswer) "addTagToCard" -> { activity.runOnUiThread { activity.showTagsDialog() } - convertToByteArray(true) + convertToByteArray(apiContract, true) } - "isInFullscreen" -> convertToByteArray(activity.isFullscreen) - "isTopbarShown" -> convertToByteArray(activity.prefShowTopbar) - "isInNightMode" -> convertToByteArray(activity.isInNightMode) + "isInFullscreen" -> convertToByteArray(apiContract, activity.isFullscreen) + "isTopbarShown" -> convertToByteArray(apiContract, activity.prefShowTopbar) + "isInNightMode" -> convertToByteArray(apiContract, activity.isInNightMode) "enableHorizontalScrollbar" -> { activity.webView!!.isHorizontalScrollBarEnabled = apiParams.toBoolean() - convertToByteArray(true) + convertToByteArray(apiContract, true) } "enableVerticalScrollbar" -> { activity.webView!!.isVerticalScrollBarEnabled = apiParams.toBoolean() - convertToByteArray(true) + convertToByteArray(apiContract, true) } else -> { - showDeveloperContact(ankiJsErrorCodeError) + showDeveloperContact(ankiJsErrorCodeError, apiContract.cardSuppliedDeveloperContact) throw Exception("unhandled request: $methodName") } } } - private fun processAction(action: () -> Boolean, errorCode: Int, conversion: (Boolean) -> ByteArray): ByteArray { + private fun processAction( + action: () -> Boolean, + apiContract: ApiContract, + errorCode: Int, + conversion: (ApiContract, Boolean) -> ByteArray + ): ByteArray { val status = action() if (!status) { - showDeveloperContact(errorCode) + showDeveloperContact(errorCode, apiContract.cardSuppliedDeveloperContact) } - return conversion(status) + return conversion(apiContract, status) } - private suspend fun ankiSearchCardWithCallback(query: String): ByteArray = withContext(Dispatchers.Main) { + private suspend fun ankiSearchCardWithCallback(apiContract: ApiContract): ByteArray = withContext(Dispatchers.Main) { val cards = try { - 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 ) - showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSearchCard) - return@withContext convertToByteArray(false) + showDeveloperContact(AnkiDroidJsAPIConstants.ankiJsErrorCodeSearchCard, apiContract.cardSuppliedDeveloperContact) + return@withContext convertToByteArray(apiContract, false) } val searchResult: MutableList = ArrayList() for (s in cards) { @@ -344,7 +342,7 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { activity.runOnUiThread { activity.webView!!.evaluateJavascript("ankiSearchCard($jsonEncodedString)", null) } - convertToByteArray(true) + convertToByteArray(apiContract, true) } open class CardDataForJsApi { @@ -366,4 +364,6 @@ open class AnkiDroidJsAPI(private val activity: AbstractFlashcardViewer) { }.toString() } } + + class ApiContract(val isValid: Boolean, val cardSuppliedDeveloperContact: String, val cardSuppliedData: String) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt index 48488b530246..772d9faacc99 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidJsAPITest.kt @@ -432,13 +432,7 @@ class AnkiDroidJsAPITest : RobolectricTest() { } fun formatApiResult(res: Any): String { - return when (res) { - is String -> "{\"success\":true,\"value\":\"$res\"}" - is Boolean -> "{\"success\":true,\"value\":\"$res\"}" - is Int -> "{\"success\":true,\"value\":\"$res\"}" - is Long -> "{\"success\":true,\"value\":\"$res\"}" - else -> "{\"success\":true,\"value\":-1}" - } + return "{\"success\":true,\"value\":\"$res\"}" } suspend fun getDataFromRequest(