1
0
mirror of https://github.com/fumiama/simple-dict-android.git synced 2026-06-05 00:30:24 +08:00
升级
1. SimpleDict (v0.1.1)
This commit is contained in:
源文雨
2025-06-22 00:25:37 +09:00
parent 638add89f6
commit 29e46651fa
5 changed files with 270 additions and 3 deletions

View File

@@ -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>

View File

@@ -18,7 +18,7 @@ android {
}
group = "top.fumiama"
version = "0.1.0"
version = "0.1.1"
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

View 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
}
}

View File

@@ -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

View 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"
}