forked from ankidroid/Anki-Android
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Anki pages host activity and server
This introduces a dependency to NanoHTTPD, a java server library Used krmanik#23 as base of this commit
- Loading branch information
Showing
8 changed files
with
269 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
* Copyright (c) 2022 Mani <[email protected]> | ||
* Copyright (c) 2022 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.pages | ||
|
||
import com.ichi2.libanki.* | ||
import com.ichi2.libanki.importer.getCsvMetadataRaw | ||
import com.ichi2.libanki.importer.importCsvRaw | ||
import com.ichi2.libanki.stats.* | ||
import fi.iki.elonen.NanoHTTPD | ||
import timber.log.Timber | ||
import java.io.ByteArrayInputStream | ||
|
||
class AnkiServer( | ||
hostname: String?, | ||
port: Int, | ||
val col: CollectionV16, | ||
val activity: PagesActivity | ||
) : NanoHTTPD(hostname, port) { | ||
|
||
override fun serve(session: IHTTPSession): Response { | ||
val uri = session.uri | ||
val mime = getMimeFromUri(uri) | ||
|
||
if (session.method == Method.GET) { | ||
Timber.d("GET: Requested %s", uri) | ||
return newChunkedResponse(Response.Status.OK, mime, this.javaClass.classLoader!!.getResourceAsStream("web$uri")) | ||
} | ||
|
||
if (session.method == Method.POST) { | ||
Timber.d("POST: Requested %s", uri) | ||
val bytes = getSessionBytes(session) | ||
if (uri.startsWith(ANKI_PREFIX)) { | ||
val data: ByteArray? = when (uri.substring(ANKI_PREFIX.length)) { | ||
"i18nResources" -> col.i18nResourcesRaw(bytes) | ||
"getGraphPreferences" -> col.getGraphPreferencesRaw() | ||
"setGraphPreferences" -> col.setGraphPreferencesRaw(bytes) | ||
"graphs" -> col.graphsRaw(bytes) | ||
"getNotetypeNames" -> col.getNotetypeNamesRaw(bytes) | ||
"getDeckNames" -> col.getDeckNamesRaw(bytes) | ||
"getCsvMetadata" -> col.getCsvMetadataRaw(bytes) | ||
"importCsv" -> col.importCsvRaw(bytes) | ||
"getFieldNames" -> col.getFieldNamesRaw(bytes) | ||
"cardStats" -> col.cardStatsRaw(bytes) | ||
else -> { Timber.w("Unhandled Anki request: %s", uri); null } | ||
} | ||
return newChunkedResponse(data) | ||
} | ||
} | ||
return newFixedLengthResponse(null) | ||
} | ||
|
||
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 | ||
} | ||
|
||
companion object { | ||
const val ANKI_PREFIX = "/_anki/" | ||
|
||
fun getMimeFromUri(uri: String): String { | ||
return when (uri.substringAfterLast(".")) { | ||
"ico" -> "image/x-icon" | ||
"css" -> "text/css" | ||
"js" -> "text/javascript" | ||
"html" -> "text/html" | ||
else -> "application/binary" | ||
} | ||
} | ||
|
||
fun newChunkedResponse( | ||
data: ByteArray?, | ||
mimeType: String = "application/binary", | ||
status: Response.IStatus = Response.Status.OK | ||
): Response { | ||
return if (data == null) { | ||
newFixedLengthResponse(null) | ||
} else { | ||
newChunkedResponse(status, mimeType, ByteArrayInputStream(data)) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
AnkiDroid/src/main/java/com/ichi2/anki/pages/PagesActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
/* | ||
* Copyright (c) 2022 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.pages | ||
|
||
import android.content.Context | ||
import android.content.Intent | ||
import android.os.Bundle | ||
import android.webkit.WebView | ||
import androidx.fragment.app.commit | ||
import com.ichi2.anki.* | ||
import com.ichi2.libanki.CollectionV16 | ||
import timber.log.Timber | ||
import java.net.ServerSocket | ||
|
||
/** | ||
* Container activity to host Anki HTML pages | ||
* * Responsibilities: | ||
* * Serve as parent activity of the [PageFragment] that holds the page | ||
* * Host an [AnkiServer] to intercept any requests made by an Anki page and resolve them | ||
* * Operate UI requests by the [AnkiServer] | ||
*/ | ||
class PagesActivity : AnkiActivity() { | ||
private lateinit var ankiServer: AnkiServer | ||
|
||
/** Port used by [ankiServer]. Normally the first available port at the moment this is instantiated */ | ||
val port = ServerSocket(0).use { socket -> socket.localPort } | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
if (showedActivityFailedScreen(savedInstanceState)) { | ||
return | ||
} | ||
super.onCreate(savedInstanceState) | ||
setContentView(R.layout.page_activity) | ||
enableToolbar() | ||
|
||
// Enable debugging on DEBUG builds | ||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) | ||
|
||
// Load server | ||
ankiServer = AnkiServer(HOST_NAME, port, col as CollectionV16, this) | ||
Timber.i("Starting server on $HOST_NAME:$port") | ||
ankiServer.start() | ||
|
||
// Launch page | ||
val pageName = intent.extras?.getString(EXTRA_PAGE_NAME) | ||
?: throw Exception("PageActivity's intent should have a '$EXTRA_PAGE_NAME' extra") | ||
|
||
val pageFragment = getPageFragment(pageName).apply { | ||
arguments = intent.getBundleExtra(EXTRA_PAGE_ARGS) | ||
} | ||
supportFragmentManager.commit { | ||
replace(R.id.page_container, pageFragment, pageName) | ||
} | ||
setTitle(pageFragment.title) | ||
} | ||
|
||
override fun onDestroy() { | ||
super.onDestroy() | ||
/** Stop running the server if the activity is destroyed. | ||
* The initialization check is for the case [showedActivityFailedScreen] is true */ | ||
if (this::ankiServer.isInitialized && ankiServer.isAlive) { | ||
ankiServer.stop() | ||
} | ||
} | ||
|
||
/** | ||
* @return the [PageFragment] whose name is equal to [pageName] | ||
* @throws Exception if there is not a page associated with the given [pageName] | ||
*/ | ||
private fun getPageFragment(pageName: String): PageFragment { | ||
return when (pageName) { | ||
CsvImporter.PAGE_NAME -> CsvImporter() | ||
else -> throw Exception("'$pageName' page doesn't have a PageFragment associated") | ||
} | ||
} | ||
|
||
companion object { | ||
/** | ||
* Extra key of [PagesActivity]'s intent that can be used to pass a [Bundle] | ||
* as arguments of the [PageFragment] that will be opened | ||
*/ | ||
const val EXTRA_PAGE_ARGS = "pageArgs" | ||
/** | ||
* Extra key of [PagesActivity]'s intent that must be included and | ||
* hold the name of an [Anki HTML page](https://github.com/ankitects/anki/tree/main/ts) | ||
*/ | ||
const val EXTRA_PAGE_NAME = "pageName" | ||
|
||
const val HOST_NAME = "127.0.0.1" | ||
|
||
/** | ||
* @param pageName name of the Anki HTML page that should be opened | ||
* @param arguments to be passed to the created [PageFragment] | ||
*/ | ||
fun getIntent(context: Context, pageName: String, arguments: Bundle? = null): Intent { | ||
return Intent(context, PagesActivity::class.java).apply { | ||
putExtra(EXTRA_PAGE_NAME, pageName) | ||
putExtra(EXTRA_PAGE_ARGS, arguments) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<androidx.constraintlayout.widget.ConstraintLayout | ||
xmlns:android="http://schemas.android.com/apk/res/android" | ||
xmlns:app="http://schemas.android.com/apk/res-auto" | ||
xmlns:tools="http://schemas.android.com/tools" | ||
android:id="@+id/root_layout" | ||
android:layout_width="match_parent" | ||
android:layout_height="match_parent" | ||
tools:context=".pages.PagesActivity"> | ||
|
||
<include layout="@layout/toolbar" /> | ||
|
||
<androidx.fragment.app.FragmentContainerView | ||
android:id="@+id/page_container" | ||
android:layout_width="0dp" | ||
android:layout_height="0dp" | ||
app:layout_constraintStart_toStartOf="parent" | ||
app:layout_constraintEnd_toEndOf="parent" | ||
app:layout_constraintTop_toBottomOf="@id/toolbar" | ||
app:layout_constraintBottom_toBottomOf="parent" /> | ||
|
||
</androidx.constraintlayout.widget.ConstraintLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters