mirror of
https://github.com/fumiama/simple-dict-android.git
synced 2026-06-05 00:30:24 +08:00
v5.1.1
升级 1. SimpleDict (v0.1.1)
This commit is contained in:
2
.idea/dictionaries/fumiama.xml
generated
2
.idea/dictionaries/fumiama.xml
generated
@@ -1,6 +1,7 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="fumiama">
|
||||
<words>
|
||||
<w>catquit</w>
|
||||
<w>eujuno</w>
|
||||
<w>karakio</w>
|
||||
<w>nisi</w>
|
||||
@@ -8,6 +9,7 @@
|
||||
<w>rjimj</w>
|
||||
<w>sdict</w>
|
||||
<w>succ</w>
|
||||
<w>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</w>
|
||||
<w>zenbi</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
||||
@@ -18,7 +18,7 @@ android {
|
||||
}
|
||||
|
||||
group = "top.fumiama"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
|
||||
mavenPublishing {
|
||||
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
|
||||
|
||||
127
sdict/src/main/java/top/fumiama/sdict/ApkUpdater.kt
Normal file
127
sdict/src/main/java/top/fumiama/sdict/ApkUpdater.kt
Normal file
@@ -0,0 +1,127 @@
|
||||
package top.fumiama.sdict
|
||||
|
||||
import top.fumiama.sdict.io.Client
|
||||
import top.fumiama.sdict.utils.Utils.toHexStr
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* A base class for checking and downloading APK updates from a remote [SimpleKanban] server.
|
||||
*
|
||||
* This class uses a [SimpleKanban] instance to communicate with the server,
|
||||
* parse versioning messages, and fetch APK binaries. It provides overridable
|
||||
* callbacks for handling update events with MD5 checksum verification.
|
||||
*
|
||||
* Subclasses can override the lifecycle methods to customize update behavior.
|
||||
*
|
||||
* @param serverIP The IP address of the update server.
|
||||
* @param serverPort The port number of the update server.
|
||||
* @param password The password used to authenticate with the server.
|
||||
*/
|
||||
open class ApkUpdater(serverIP: String, serverPort: Int, password: String) {
|
||||
|
||||
/** Client used for network communication with the server. */
|
||||
private val client = Client(serverIP, serverPort)
|
||||
|
||||
/** Wrapper around the client to handle kanban protocol operations. */
|
||||
private val kanban = SimpleKanban(client, password)
|
||||
|
||||
/**
|
||||
* Called when a newer app version or plain message is available from the server.
|
||||
*
|
||||
* The MD5 is only been provided when new APK is available.
|
||||
*
|
||||
* @param version The new version number.
|
||||
* @param message A APK changelog or developer notice.
|
||||
* @param md5 Optional MD5 checksum of the APK file, if provided.
|
||||
*/
|
||||
open suspend fun onCheckNewVersion(version: Int, message: String, md5: String? = null) {}
|
||||
|
||||
/**
|
||||
* Called when the current version is already the latest.
|
||||
*
|
||||
* @param version The current installed version.
|
||||
*/
|
||||
open suspend fun onCheckLatestVersion(version: Int) {}
|
||||
|
||||
/**
|
||||
* Called when APK downloading fails.
|
||||
*
|
||||
* @param cause The failure reason, either [UPDATE_FAIL_NETWORK] or [UPDATE_FAIL_FILE_CORRUPT].
|
||||
*/
|
||||
open suspend fun onDownloadNewVersionFailed(cause: Int) {}
|
||||
|
||||
/**
|
||||
* Called when the APK file is successfully downloaded and verified.
|
||||
*
|
||||
* @param data The binary contents of the downloaded APK file.
|
||||
*/
|
||||
open suspend fun onDownloadNewVersionSuccess(data: ByteArray) {}
|
||||
|
||||
/**
|
||||
* Checks with the server if a new version is available.
|
||||
*
|
||||
* Parses the message format to determine:
|
||||
* - If the current version is the latest (`"null"` response)
|
||||
* - If a newer version exists with/without an MD5 checksum
|
||||
*
|
||||
* Example response format:
|
||||
* ```
|
||||
* 5
|
||||
* Update message here
|
||||
* md5:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
* ```
|
||||
*
|
||||
* @param currentVersion The current installed version number.
|
||||
*/
|
||||
suspend fun check(currentVersion: Int) {
|
||||
val msg = kanban[currentVersion]
|
||||
if (msg == "null") {
|
||||
onCheckLatestVersion(currentVersion)
|
||||
return
|
||||
}
|
||||
val verNum = msg.substringBefore('\n').toIntOrNull() ?: return
|
||||
if (!msg.contains("md5:")) {
|
||||
onCheckNewVersion(verNum, msg.substringAfter('\n'))
|
||||
return
|
||||
}
|
||||
onCheckNewVersion(
|
||||
verNum,
|
||||
msg.substringAfter('\n').substringBeforeLast('\n'),
|
||||
msg.substringAfterLast("md5:")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the APK binary and validates its MD5 checksum.
|
||||
*
|
||||
* If the checksum is valid, the update is considered successful and
|
||||
* [onDownloadNewVersionSuccess] is called. Otherwise,
|
||||
* [onDownloadNewVersionFailed] is invoked with an appropriate error code.
|
||||
*
|
||||
* @param md5 The expected MD5 hash of the APK file.
|
||||
* @param progressHandler Optional handler to track download progress.
|
||||
*/
|
||||
suspend fun download(md5: String, progressHandler: Client.Progress) {
|
||||
client.progress = progressHandler
|
||||
try {
|
||||
kanban.fetch({ onDownloadNewVersionFailed(UPDATE_FAIL_NETWORK) }) {
|
||||
if (md5 == toHexStr(
|
||||
MessageDigest.getInstance("MD5").digest(it)
|
||||
)
|
||||
) onDownloadNewVersionSuccess(it)
|
||||
else onDownloadNewVersionFailed(UPDATE_FAIL_FILE_CORRUPT)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
client.progress = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Error code indicating a network failure during the update process. */
|
||||
const val UPDATE_FAIL_NETWORK = 0
|
||||
|
||||
/** Error code indicating MD5 mismatch after download. */
|
||||
const val UPDATE_FAIL_FILE_CORRUPT = 1
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,8 @@ import top.fumiama.sdict.protocol.SimpleProtobuf
|
||||
import top.fumiama.sdict.protocol.Tea
|
||||
|
||||
/**
|
||||
* A high-level dictionary manager that communicates with a remote server over a custom protocol via [Client].
|
||||
* A high-level dictionary manager that communicates with a remote [Simple Dict Server](https://github.com/fumiama/simple-dict)
|
||||
* over a custom protocol via [Client].
|
||||
*
|
||||
* This class supports fetching, storing, deleting, and checking remote dictionary entries. It maintains a local cache,
|
||||
* synchronizes updates, and verifies integrity using MD5.
|
||||
@@ -167,7 +168,7 @@ class SimpleDict(
|
||||
CmdPacket(CmdPacket.CMD_MD5, md5, teaPassword).encrypt(seq++)
|
||||
)
|
||||
val cp = ack
|
||||
Log.d("SimpleDict", "Check md5: $cp")
|
||||
Log.d("SimpleDict", "check md5: $cp")
|
||||
closeDict()
|
||||
cp == "nequ"
|
||||
} else false
|
||||
|
||||
137
sdict/src/main/java/top/fumiama/sdict/SimpleKanban.kt
Normal file
137
sdict/src/main/java/top/fumiama/sdict/SimpleKanban.kt
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* SimpleKanban.kt
|
||||
*
|
||||
* Copyright (C) 2025 Minamoto Fumiama
|
||||
*
|
||||
* 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, version 3.
|
||||
*
|
||||
* 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 https://www.gnu.org/licenses/.
|
||||
*/
|
||||
package top.fumiama.sdict
|
||||
|
||||
import android.util.Log
|
||||
import top.fumiama.sdict.io.Client
|
||||
|
||||
/**
|
||||
* A client-side utility class for interacting with a
|
||||
* [Simple Kanban Server](https://github.com/fumiama/simple-kanban).
|
||||
* This class is designed to fetch raw kanban data or version-specific string responses
|
||||
* from a server using a custom binary-text-mixed protocol. It must be executed in a separate thread
|
||||
* or coroutine due to network I/O operations.
|
||||
*
|
||||
* @param client A pre-configured [Client] instance used to manage the socket connection.
|
||||
* @param password A password string used for authentication when sending commands to the server.
|
||||
* Please note that the password is sent in cleartext without any encryption.
|
||||
*/
|
||||
class SimpleKanban(private val client: Client, private val password: String) {
|
||||
|
||||
/**
|
||||
* Attempts to retrieve raw kanban data from the server.
|
||||
*
|
||||
* The client sends a specific command using the password and waits for a response,
|
||||
* expecting a 4-byte LE header to determine the full message length, then reads the full payload.
|
||||
*
|
||||
* This process is retried up to 3 times if an exception occurs during reading.
|
||||
*
|
||||
* @return A [ByteArray] containing the raw data if successful, or `null` if all attempts fail.
|
||||
*/
|
||||
private val raw: ByteArray?
|
||||
get() {
|
||||
var times = 3
|
||||
var re: ByteArray
|
||||
var firstReceived: ByteArray
|
||||
do {
|
||||
re = byteArrayOf()
|
||||
if(client.initConnect()) {
|
||||
client.sendMessage("${password}catquit") // Send command to request raw data.
|
||||
client.receiveRawMessage(33) // Welcome to simple kanban server.
|
||||
try {
|
||||
firstReceived = client.receiveRawMessage(4) // Read header to get length.
|
||||
val length = convert2Int(firstReceived)
|
||||
Log.d("SimpleKanban", "raw length: $length")
|
||||
// Handle any additional bytes beyond the header in the same buffer.
|
||||
if(firstReceived.size > 4)
|
||||
re += firstReceived.copyOfRange(4, firstReceived.size)
|
||||
// Read remaining bytes based on calculated total length.
|
||||
re += client.receiveRawMessage(length - re.size, setProgress = true)
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
client.closeConnect()
|
||||
}
|
||||
} while (times-- > 0)
|
||||
return if(re.isEmpty()) null else re
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a 4-byte little-endian byte array to an integer.
|
||||
*
|
||||
* The input is expected to be in little-endian format.
|
||||
*
|
||||
* @param buffer A 4-byte array containing the length in little-endian order.
|
||||
* @return The converted integer value.
|
||||
*/
|
||||
private fun convert2Int(buffer: ByteArray) =
|
||||
(buffer[3].toInt() and 0xff shl 24) or
|
||||
(buffer[2].toInt() and 0xff shl 16) or
|
||||
(buffer[1].toInt() and 0xff shl 8) or
|
||||
(buffer[0].toInt() and 0xff)
|
||||
|
||||
/**
|
||||
* Asynchronously fetches the raw kanban data and handles success or failure.
|
||||
*
|
||||
* This method is suspendable and should be called within a coroutine context.
|
||||
*
|
||||
* @param doOnLoadFailure Called if the fetch operation fails or receives no data.
|
||||
* @param doOnLoadSuccess Called with the received [ByteArray] if the fetch succeeds.
|
||||
*/
|
||||
suspend fun fetch(
|
||||
doOnLoadFailure: suspend () -> Unit,
|
||||
doOnLoadSuccess: suspend (data: ByteArray) -> Unit
|
||||
) {
|
||||
raw?.let { doOnLoadSuccess(it) } ?: doOnLoadFailure()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a specific kanban version string from the server.
|
||||
*
|
||||
* The client sends a versioned request using the password and receives a response.
|
||||
* The response may be a 4-byte "null" message or a length-prefixed string.
|
||||
* In case of errors or connection failure, `"null"` is returned.
|
||||
*
|
||||
* @param version An integer identifying local kanban board version.
|
||||
* @return The corresponding remote kanban data as a [String], or `"null"` if not found or failed.
|
||||
*/
|
||||
operator fun get(version: Int): String =
|
||||
if(client.initConnect()) {
|
||||
client.sendMessage("${password}get${version}quit") // Send version-specific request.
|
||||
client.receiveRawMessage(36) // Welcome to simple kanban server. get
|
||||
val r = try {
|
||||
val firstReceive = client.receiveRawMessage(4)
|
||||
if(firstReceive.decodeToString() == "null") "null"
|
||||
else {
|
||||
val length = convert2Int(firstReceive)
|
||||
Log.d("SimpleKanban", "get length: $length")
|
||||
var re = byteArrayOf()
|
||||
if(firstReceive.size > 4)
|
||||
re += firstReceive.copyOfRange(4, firstReceive.size)
|
||||
re += client.receiveRawMessage(length - re.size)
|
||||
if(re.isNotEmpty()) re.decodeToString() else "null"
|
||||
}
|
||||
} catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
"null"
|
||||
}
|
||||
client.closeConnect()
|
||||
r
|
||||
} else "null"
|
||||
}
|
||||
Reference in New Issue
Block a user