Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
AnkiDroid/src/main/assets/mathjax
AnkiDroid/src/main/assets/jquery.min.js
AnkiDroid/src/main/assets/scripts/ankidroid-js-api.js
AnkiDroid/build/
1 change: 1 addition & 0 deletions AnkiDroid/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
-keep class androidx.core.app.ActivityCompat$* { *; }
-keep class androidx.concurrent.futures.** { *; }
-keep class androidx.appcompat.view.menu.MenuItemImpl { *; } # .utils.ext.MenuItemImpl
-keep class com.ichi2.anki.jsapi.Endpoint { *; } # allEndpoints

# Ignore unused packages
-dontwarn javax.naming.**
Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/assets/scripts/ankidroid-js-api.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import com.ichi2.anki.dialogs.TtsVoicesDialogFragment
import com.ichi2.anki.dialogs.tags.TagsDialog
import com.ichi2.anki.dialogs.tags.TagsDialogFactory
import com.ichi2.anki.dialogs.tags.TagsDialogListener
import com.ichi2.anki.jsapi.JsApi
import com.ichi2.anki.libanki.Card
import com.ichi2.anki.libanki.CardId
import com.ichi2.anki.libanki.Collection
Expand Down Expand Up @@ -2721,9 +2722,9 @@ abstract class AbstractFlashcardViewer :
uri: String,
bytes: ByteArray,
): ByteArray =
if (uri.startsWith(AnkiServer.ANKIDROID_JS_PREFIX)) {
if (uri.startsWith(JsApi.REQUEST_PREFIX)) {
jsApi.handleJsApiRequest(
uri.substring(AnkiServer.ANKIDROID_JS_PREFIX.length),
uri.substring(JsApi.REQUEST_PREFIX.length),
bytes,
returnDefaultValues = true,
)
Expand Down
6 changes: 3 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import com.ichi2.anki.cardviewer.Gesture
import com.ichi2.anki.cardviewer.ViewerCommand
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.common.time.TimeManager
import com.ichi2.anki.jsapi.JsApi
import com.ichi2.anki.libanki.Card
import com.ichi2.anki.libanki.CardId
import com.ichi2.anki.libanki.Collection
Expand All @@ -89,7 +90,6 @@ import com.ichi2.anki.multimedia.audio.AudioRecordingController.Companion.tempAu
import com.ichi2.anki.multimedia.audio.AudioRecordingController.RecordingState
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import com.ichi2.anki.observability.undoableOp
import com.ichi2.anki.pages.AnkiServer.Companion.ANKIDROID_JS_PREFIX
import com.ichi2.anki.pages.AnkiServer.Companion.ANKI_PREFIX
import com.ichi2.anki.pages.CardInfoDestination
import com.ichi2.anki.preferences.sharedPrefs
Expand Down Expand Up @@ -1590,9 +1590,9 @@ open class Reviewer :
"i18nResources" -> withCol { i18nResourcesRaw(bytes) }
else -> throw IllegalArgumentException("unhandled request: $methodName")
}
} else if (uri.startsWith(ANKIDROID_JS_PREFIX)) {
} else if (uri.startsWith(JsApi.REQUEST_PREFIX)) {
jsApi.handleJsApiRequest(
uri.substring(ANKIDROID_JS_PREFIX.length),
uri.substring(JsApi.REQUEST_PREFIX.length),
bytes,
returnDefaultValues = false,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class MediaErrorHandler {
private var hasExecuted = false

private var automaticTtsFailureCount = 0
private var hasShownInvalidContractMessage = false

fun processFailure(
request: WebResourceRequest,
Expand Down Expand Up @@ -103,4 +104,10 @@ class MediaErrorHandler {

errorHandler.invoke(error)
}

fun shouldShowJsApiExceptionMessage(): Boolean {
if (hasShownInvalidContractMessage) return false
hasShownInvalidContractMessage = true
return true
}
}
182 changes: 182 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/jsapi/Endpoint.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright (c) 2025 Brayan Oliveira <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.jsapi

/**
* Represents a JavaScript API Endpoint.
*
* It should be structured as `service-base/endpoint`
*/
sealed interface Endpoint {
/** Base path of the service */
val base: String
val value: String

enum class Android(
override val value: String,
) : Endpoint {
SHOW_SNACKBAR("show-snackbar"),
IS_SYSTEM_IN_DARK_MODE("is-system-in-dark-mode"),
IS_NETWORK_METERED("is-network-metered"),
;

override val base: String = "android"
}

enum class Card(
override val value: String,
) : Endpoint {
GET_ID("get-id"),
GET_FLAG("get-flag"),
GET_REPS("get-reps"),
GET_INTERVAL("get-interval"),
GET_FACTOR("get-factor"),
GET_MOD("get-mod"),
GET_NID("get-nid"),
GET_TYPE("get-type"),
GET_DID("get-did"),
GET_LEFT("get-left"),
GET_O_DID("get-o-did"),
GET_O_DUE("get-o-due"),
GET_QUEUE("get-queue"),
GET_LAPSES("get-lapses"),
GET_DUE("get-due"),
GET_QUESTION("get-question"),
GET_ANSWER("get-answer"),
IS_MARKED("is-marked"),
BURY("bury"),
SUSPEND("suspend"),
UNBURY("unbury"),
UNSUSPEND("unsuspend"),
RESET_PROGRESS("reset-progress"),
TOGGLE_FLAG("toggle-flag"),
GET_REVIEW_LOGS("get-review-logs"),
;

override val base = "card"
}

enum class Collection(
override val value: String,
) : Endpoint {
UNDO("undo"),
REDO("redo"),
IS_UNDO_AVAILABLE("is-undo-available"),
IS_REDO_AVAILABLE("is-redo-available"),
FIND_CARDS("find-cards"),
FIND_NOTES("find-notes"),
;

override val base = "collection"
}

enum class Deck(
override val value: String,
) : Endpoint {
GET_ID("get-id"),
GET_NAME("get-name"),
IS_FILTERED("is-filtered"),
;

override val base = "deck"
}

enum class Note(
override val value: String,
) : Endpoint {
GET_ID("get-id"),
GET_NOTE_TYPE_ID("get-note-type-id"),
GET_CARD_IDS("get-card-ids"),
BURY("bury"),
SUSPEND("suspend"),
GET_TAGS("get-tags"),
SET_TAGS("set-tags"),
TOGGLE_MARK("toggle-mark"),
;

override val base = "note"
}

enum class NoteType(
override val value: String,
) : Endpoint {
GET_ID("get-id"),
GET_NAME("get-name"),
IS_IMAGE_OCCLUSION("is-image-occlusion"),
IS_CLOZE("is-cloze"),
GET_FIELD_NAMES("get-field-names"),
;

override val base = "note-type"
}

enum class StudyScreen(
override val value: String,
) : Endpoint {
GET_NEW_COUNT("get-new-count"),
GET_LEARNING_COUNT("get-learning-count"),
GET_TO_REVIEW_COUNT("get-to-review-count"),
SHOW_ANSWER("show-answer"),
ANSWER("answer"),
IS_SHOWING_ANSWER("is-showing-answer"),
GET_NEXT_TIME("get-next-time"),
OPEN_CARD_INFO("open-card-info"),
OPEN_NOTE_EDITOR("open-note-editor"),
SET_BACKGROUND_COLOR("set-background-color"),
DELETE_NOTE("delete-note"),
;

override val base = "study-screen"
}

enum class Tts(
override val value: String,
) : Endpoint {
SPEAK("speak"),
SET_LANGUAGE("set-language"),
SET_PITCH("set-pitch"),
SET_SPEECH_RATE("set-speech-rate"),
IS_SPEAKING("is-speaking"),
STOP("stop"),
;

override val base = "tts"
}

companion object {
/**
* A map of all possible endpoints, indexed by a pair of their base and value strings.
*/
private val allEndpoints by lazy {
Endpoint::class
.sealedSubclasses
.flatMap { it.java.enumConstants?.asList() ?: emptyList() }
.associateBy { it.base to it.value }
}

/**
* Retrieves a specific Endpoint enum constant based on its base and value.
*
* @param base The base string of the endpoint (e.g., "card").
* @param value The value string of the endpoint (e.g., "get-id").
* @return The matching [Endpoint], or `null` if no match is found.
*/
fun from(
base: String,
value: String,
): Endpoint? = allEndpoints[base to value]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Brayan Oliveira <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A

* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.jsapi

import android.content.res.Resources
import com.ichi2.anki.R

sealed class InvalidContractException : Exception() {
abstract fun localizedErrorMessage(resources: Resources): String

class ContactError : InvalidContractException() {
override fun localizedErrorMessage(resources: Resources): String {
val errorMessage = resources.getString(R.string.js_api_error_code, INVALID_CONTACT_ERROR_CODE)
return resources.getString(R.string.invalid_contact_message, errorMessage)
}
}

class VersionError(
private val requestVersion: String,
private val contact: String,
) : InvalidContractException() {
override fun localizedErrorMessage(resources: Resources): String {
val errorMessage = resources.getString(R.string.js_api_error_code, INVALID_VERSION_ERROR_CODE)
return resources.getString(R.string.invalid_js_api_version_message, requestVersion, contact, errorMessage)
}
}

class OutdatedVersion(
private val currentVersion: String,
private val requestVersion: String,
private val contact: String,
) : InvalidContractException() {
override fun localizedErrorMessage(resources: Resources): String {
val errorMessage = resources.getString(R.string.js_api_error_code, OUTDATED_VERSION_ERROR_CODE)
return resources.getString(R.string.outdated_js_api_message, currentVersion, requestVersion, contact, errorMessage)
}
}

companion object {
const val INVALID_CONTACT_ERROR_CODE = "INVALID_CONTACT"
const val INVALID_VERSION_ERROR_CODE = "INVALID_VERSION"
const val OUTDATED_VERSION_ERROR_CODE = "OUTDATED_VERSION"
}
}
Loading
Loading