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(