Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting card info #23

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
13 changes: 4 additions & 9 deletions AnkiDroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -273,15 +273,8 @@ dependencies {
if (project.rootProject.file('local.properties').exists()) {
localProperties.load(project.rootProject.file('local.properties').newDataInputStream())
}
if (localProperties['local_backend'] == "true") {
implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar")
testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-${ankidroid_backend_version}.jar")
// On Windows, you can use something like
// implementation files("C:\\GitHub\\Rust-Test\\rsdroid\\build\\outputs\\aar\\rsdroid-release.aar")
} else {
implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version"
testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version"
}

implementation files("C:\\rsdroid\\rsdroid-release.aar")

// A path for a testing library which provide Parameterized Test
testImplementation "org.junit.jupiter:junit-jupiter:$junit_version"
Expand Down Expand Up @@ -315,6 +308,8 @@ dependencies {
// build via AnkiDroidApp.
implementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'

implementation 'org.nanohttpd:nanohttpd:2.3.1'

api project(":api")

testImplementation "org.junit.vintage:junit-vintage-engine:$junit_version"
Expand Down
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,12 @@
android:label="@string/download_deck"
android:theme="@style/Theme.MaterialComponents.NoActionBar" />

<activity
android:name="com.ichi2.anki.pages.AnkiPagesWebview"
android:configChanges="keyboardHidden|orientation|screenSize|locale"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>

<!-- Service to perform web API queries -->
<service android:name="com.ichi2.widget.AnkiDroidWidgetSmall$UpdateService" />

Expand Down
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,11 @@ open class DeckPicker :
if (!importResult.isSuccess) {
ImportUtils.showImportUnsuccessfulDialog(this, importResult.humanReadableMessage, false)
}
} else if (requestCode == PICK_TEXT_FILE && resultCode == RESULT_OK) {
val importResult = ImportUtils.handleFileImport(this, data!!)
if (!importResult.isSuccess) {
ImportUtils.showImportUnsuccessfulDialog(this, importResult.humanReadableMessage, false)
}
}
}

Expand Down Expand Up @@ -2550,6 +2555,7 @@ open class DeckPicker :
const val SHOW_STUDYOPTIONS = 11
private const val ADD_NOTE = 12
const val PICK_APKG_FILE = 13
const val PICK_TEXT_FILE = 14

// For automatic syncing
// 10 minutes in milliseconds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.drakeet.drawer.FullDraggableContainer
import com.google.android.material.navigation.NavigationView
import com.ichi2.anim.ActivityTransitionAnimation.Direction.*
import com.ichi2.anki.dialogs.HelpDialog
import com.ichi2.anki.pages.AnkiPagesWebview
import com.ichi2.themes.Themes
import com.ichi2.utils.HandlerUtils
import com.ichi2.utils.KotlinCleanup
Expand Down Expand Up @@ -295,7 +296,11 @@ abstract class NavigationDrawerActivity :
openCardBrowser()
} else if (itemId == R.id.nav_stats) {
Timber.i("Navigating to stats")
val intent = Intent(this@NavigationDrawerActivity, Statistics::class.java)
// val intent = Intent(this@NavigationDrawerActivity, Statistics::class.java)
// startActivityForResultWithAnimation(intent, REQUEST_STATISTICS, START)
val intent = Intent(this@NavigationDrawerActivity, AnkiPagesWebview::class.java)
intent.putExtra("cardId", currentCardId)
intent.putExtra("web_page", "graphs")
startActivityForResultWithAnimation(intent, REQUEST_STATISTICS, START)
} else if (itemId == R.id.nav_settings) {
Timber.i("Navigating to settings")
Expand Down
4 changes: 3 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import com.ichi2.anki.dialogs.RescheduleDialog.rescheduleSingleCard
import com.ichi2.anki.multimediacard.AudioView
import com.ichi2.anki.multimediacard.AudioView.Companion.createRecorderInstance
import com.ichi2.anki.multimediacard.AudioView.Companion.generateTempAudioFile
import com.ichi2.anki.pages.AnkiPagesWebview
import com.ichi2.anki.reviewer.*
import com.ichi2.anki.reviewer.AnswerButtons.Companion.getBackgroundColors
import com.ichi2.anki.reviewer.AnswerButtons.Companion.getTextColors
Expand Down Expand Up @@ -688,10 +689,11 @@ open class Reviewer : AbstractFlashcardViewer() {
showThemedToast(this, getString(R.string.multimedia_editor_something_wrong), true)
return
}
val intent = Intent(this, CardInfo::class.java)
val intent = Intent(this, AnkiPagesWebview::class.java)
val animation = getAnimationTransitionFromGesture(fromGesture)
intent.putExtra("cardId", mCurrentCard!!.id)
intent.putExtra(FINISH_ANIMATION_EXTRA, getInverseTransition(animation) as Parcelable)
intent.putExtra("web_page", "card-info")
startActivityWithAnimation(intent, animation)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ object UsageAnalytics {
@AnalyticsConstant
val EXCEPTION_REPORT = "Exception Report"

@AnalyticsConstant
val IMPORT_TEXT_FILE = "Import TEXT"

@AnalyticsConstant
val IMPORT_APKG_FILE = "Import APKG"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,19 @@ class ImportFileSelectionFragment {
// this needs a deckPicker for now. See use of PICK_APKG_FILE

// This is required for serialization of the lambda
class OpenFilePicker(var multiple: Boolean = false) : FunctionItem.ActivityConsumer {
class OpenFilePicker(var multiple: Boolean = false, var text: Boolean = false) : FunctionItem.ActivityConsumer {
override fun consume(activity: AnkiActivity) {
openImportFilePicker(activity, multiple)
openImportFilePicker(activity, multiple, text)
}
}

val importItems = arrayListOf<RecursivePictureMenu.Item>(
FunctionItem(
R.string.import_text_file,
R.drawable.ic_manual_black_24dp,
UsageAnalytics.Actions.IMPORT_TEXT_FILE,
OpenFilePicker()
),
FunctionItem(
R.string.import_deck_package,
R.drawable.ic_manual_black_24dp,
Expand All @@ -62,7 +68,7 @@ class ImportFileSelectionFragment {

// needs to be static for serialization
@JvmStatic
fun openImportFilePicker(activity: AnkiActivity, multiple: Boolean = false) {
fun openImportFilePicker(activity: AnkiActivity, multiple: Boolean = false, text: Boolean = false) {
Timber.d("openImportFilePicker() delegating to file picker intent")
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
Expand All @@ -71,7 +77,9 @@ class ImportFileSelectionFragment {
intent.putExtra("android.content.extra.FANCY", true)
intent.putExtra("android.content.extra.SHOW_FILESIZE", true)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
activity.startActivityForResultWithoutAnimation(intent, DeckPicker.PICK_APKG_FILE)

val apkgFile = if (text) DeckPicker.PICK_TEXT_FILE else DeckPicker.PICK_APKG_FILE
activity.startActivityForResultWithoutAnimation(intent, apkgFile)
}
}
}
157 changes: 157 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiNanoHTTPD.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/***************************************************************************************
* Copyright (c) 2022 Ankitects Pty Ltd <http://apps.ankiweb.net> *
* *
* 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.pages

import android.content.Context
import android.content.SharedPreferences
import com.ichi2.libanki.Collection
import com.ichi2.libanki.importer.*
import com.ichi2.libanki.stats.*
import fi.iki.elonen.NanoHTTPD
import timber.log.Timber
import java.io.ByteArrayInputStream

class AnkiNanoHTTPD : NanoHTTPD {
private var context: Context
var sharedPreferences: SharedPreferences? = null
var col: Collection? = null

constructor(port: Int, context: Context) : super(port) {
this.context = context
}

constructor(hostname: String?, port: Int, context: Context, col: Collection) : super(hostname, port) {
this.context = context
this.col = col
}

override fun serve(session: IHTTPSession): Response {
val uri = session.uri
val method = session.method

val mime = when (uri.substringAfterLast(".")) {
"ico" -> "image/x-icon"
"css" -> "text/css"
"js" -> "text/javascript"
"html" -> "text/html"
else -> "application/binary"
}

if (method == Method.GET) {
return newChunkedResponse(
Response.Status.OK,
mime,
this.javaClass.classLoader!!.getResourceAsStream("web$uri")
)
}

if (method == Method.POST) {
Timber.d("Requested %s", uri)

when (uri) {
"/_anki/i18nResources" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.i18nResourcesRaw(bytes))
)
}
"/_anki/getGraphPreferences" -> {
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.getGraphPreferencesRaw())
)
}
"/_anki/setGraphPreferences" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.setGraphPreferencesRaw(bytes))
)
}
"/_anki/graphs" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.graphsRaw(bytes))
)
}
"/_anki/getNotetypeNames" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.getNotetypeNamesRaw(bytes))
)
}
"/_anki/getDeckNames" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.getDeckNamesRaw(bytes))
)
}
"/_anki/getCsvMetadata" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.getCsvMetadataRaw(bytes))
)
}
"/_anki/importCsv" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.importCsvRaw(bytes))
)
}
"/_anki/getFieldNames" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.getFieldNamesRaw(bytes))
)
}
"/_anki/cardStats" -> {
val bytes = getSessionBytes(session)
return newChunkedResponse(
Response.Status.OK,
mime,
ByteArrayInputStream(col?.newBackend?.cardStatsRaw(bytes))
)
}
}
}

return newFixedLengthResponse("")
}

private fun getSessionBytes(session: IHTTPSession): ByteArray {
val contentLength = session.headers["content-length"]!!.toInt()
val bytes = ByteArray(contentLength)
session.inputStream.read(bytes, 0, contentLength)
return bytes
}
}
85 changes: 85 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiPagesWebview.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/***************************************************************************************
* Copyright (c) 2022 Ankitects Pty Ltd <http://apps.ankiweb.net> *
* *
* 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.pages

import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import com.ichi2.anki.AnkiActivity
import com.ichi2.anki.R
import com.ichi2.themes.Themes
import timber.log.Timber
import java.io.IOException
import java.net.ServerSocket

class AnkiPagesWebview : AnkiActivity() {
private lateinit var ankiServer: AnkiNanoHTTPD
private lateinit var webview: WebView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.anki_pages_webview)
webview = findViewById(R.id.anki_wb)

val port = startServer()
val webPageName: String = intent.getStringExtra("web_page").toString()
val cardId: Long = intent.getLongExtra("cardId", -1)
val path: String = intent.getStringExtra("csv_path").toString()
val nightMode = if (Themes.currentTheme.isNightMode) "#night" else ""

webview.settings.javaScriptEnabled = true
webview.webChromeClient = WebChromeClient()
webview.webViewClient = AnkiWebChromeClient(webPageName, cardId, path)
webview.loadUrl("http://127.0.0.1:$port/$webPageName.html$nightMode")
}

// start server
private fun startServer(): Int {
val port = getAvailablePort()
ankiServer = AnkiNanoHTTPD("127.0.0.1", port, applicationContext, col)
ankiServer.start()
Timber.i("Running server on 127.0.0.1$port")
return port
}

@Throws(IOException::class)
private fun getAvailablePort(): Int {
ServerSocket(0).use { socket -> return socket.localPort }
}

override fun onDestroy() {
super.onDestroy()
if (ankiServer.isAlive) {
ankiServer.stop()
}
}

class AnkiWebChromeClient(private val webPageName: String, private val cardId: Long, private val path: String) : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
if (webPageName == "import-csv") {
view?.evaluateJavascript("anki.setupImportCsvPage('$path');", null)
}

if (webPageName == "card-info") {
view?.evaluateJavascript("anki.cardInfoPromise = anki.setupCardInfo(document.body);", null)
view?.evaluateJavascript("anki.cardInfoPromise.then((c) => c.\$set({cardId: $cardId}));", null)
}
}
}
}
Loading