1
0
mirror of https://github.com/fumiama/simple-dict-android.git synced 2026-06-17 00:57:29 +08:00
优化
1. 将所有线程改为协程
2. 模块化 SimpleDict (v0.1.0)
修复
1. jcenter 失效
This commit is contained in:
源文雨
2025-06-16 00:27:28 +09:00
parent b1abd53f17
commit 638add89f6
33 changed files with 1377 additions and 733 deletions

View File

@@ -9,9 +9,10 @@ android {
compileSdk 34
applicationId "top.fumiama.simpledict"
minSdkVersion 26
//noinspection OldTargetApi
targetSdkVersion 34
versionCode 22
versionName '5.0.2'
versionCode 23
versionName '5.1.0'
resConfigs "zh", "zh-rCN"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -44,14 +45,14 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation project(':sdict')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'com.lapism:search:2.4.1@aar'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation files('libs/com.lapism/search-2.4.1.aar') // https://stackoverflow.com/a/63029110/28801553
}

Binary file not shown.

View File

@@ -1,24 +0,0 @@
package top.fumiama.simpledict
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("top.fumiama.simpledict", appContext.packageName)
}
}

View File

@@ -1,27 +0,0 @@
package top.fumiama.simpledict
//Fumiama 20210601
//ByteArrayQueue.kt
//FIFO队列
class ByteArrayQueue {
private var elements = byteArrayOf()
val size get() = elements.size
fun append(items: ByteArray) {
elements += items
}
fun pop(num: Int = 1): ByteArray? {
return if(num <= elements.size) {
val re = elements.copyOfRange(0, num)
elements = elements.copyOfRange(num, elements.size)
re
} else null
}
fun clear() {
elements = byteArrayOf()
}
fun popAll(): ByteArray {
val re = elements
clear()
return re
}
operator fun plusAssign(items: ByteArray) = append(items)
}

View File

@@ -1,119 +0,0 @@
package top.fumiama.simpledict
//Fumiama 20210601
//Client.kt
import android.util.Log
import top.fumiama.simpledict.Utils.toHexStr
import java.io.*
import java.lang.Thread.sleep
import java.net.Socket
class Client(private val ip: String, private val port: Int) {
//普通数据交互接口
private var sc: Socket? = null
//普通交互流
private var dout: OutputStream? = null
private var din: InputStream? = null
//已连接标记
private val isConnect get() = sc != null && din != null && dout != null
/**
* 初始化普通交互连接
*/
fun initConnect(depth: Int = 0): Boolean{
if(depth > 3) Log.d("MyC", "connect server failed after $depth tries")
else try {
sc = Socket(ip, port) //通过socket连接服务器
din = sc?.getInputStream() //获取输入流并转换为StreamReader约定编码格式
dout = sc?.getOutputStream() //获取输出流
sc?.soTimeout = 10000 //设置连接超时限制
return if (isConnect) {
Log.d("MyC", "connect server successful")
true
} else {
Log.d("MyC", "connect server failed, now retry...")
initConnect(depth + 1)
}
} catch (e: IOException) { //获取输入输出流是可能报IOException的所以必须try-catch
e.printStackTrace()
}
return false
}
/**
* 发送数据至服务器
* @param message 要发送至服务器的字符串
*/
fun sendMessage(message: String?): Boolean = sendMessage(message?.toByteArray())
fun sendMessage(message: ByteArray?): Boolean {
try {
if (isConnect) {
if (message != null) { //判断输出流或者消息是否为空为空的话会产生null pointer错误
dout?.write(message)
dout?.flush()
Log.d("MyC", "Send msg: ${toHexStr(message)}")
return true
} else Log.d("MyC", "The message to be sent is empty")
Log.d("MyC", "send message succeed")
} else Log.d("MyC", "send message failed: no connect")
} catch (e: IOException) {
Log.d("MyC", "send message failed: crash")
e.printStackTrace()
}
return false
}
fun read(): Char? = din?.read()?.toChar()
private var buffer = ByteArrayQueue()
private val receiveBuffer = ByteArray(65536)
fun receiveRawMessage(totalSize: Int, setProgress: Boolean = false) : ByteArray {
if(totalSize == buffer.size) return buffer.popAll()
else {
try {
if (isConnect) {
Log.d("MyC", "开始接收服务端信息")
while(totalSize > buffer.size) {
val count = din?.read(receiveBuffer)?:0
if(count > 0) {
buffer += receiveBuffer.copyOfRange(0, count)
Log.d("MyC", "reply length:$count")
if(setProgress && totalSize > 0) progress?.notify(100 * buffer.size / totalSize)
} else sleep(10)
}
} else Log.d("MyC", "no connect to receive message")
} catch (e: IOException) {
Log.d("MyC", "receive message failed")
e.printStackTrace()
}
return if(totalSize > 0) buffer.pop(totalSize)?:byteArrayOf() else buffer.popAll()
}
}
//fun receiveMessage(totalSize: Int) = receiveRawMessage(totalSize).decodeToString()
/**
* 关闭连接
*/
fun closeConnect() = try {
din?.close()
dout?.close()
sc?.close()
sc = null
din = null
dout = null
true
} catch (e: IOException) {
e.printStackTrace()
false
}
var progress: Progress? = null
interface Progress {
fun notify(progressPercentage: Int)
}
}

View File

@@ -1,56 +0,0 @@
package top.fumiama.simpledict;
import android.util.Log;
import androidx.annotation.NonNull;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class CmdPacket {
private final byte cmd;
private final byte[] data;
private final byte[] md5;
private final Tea t;
public CmdPacket(byte cmd, @NonNull byte[] data, @NonNull Tea t) throws NoSuchAlgorithmException {
this.cmd = cmd;
this.data = data;
this.t = t;
md5 = MessageDigest.getInstance("MD5").digest(data);
Log.d("MyCP", "md5: "+Utils.INSTANCE.toHexStr(md5));
}
public CmdPacket(@NonNull byte[] raw, @NonNull Tea t) {
this.cmd = raw[0];
this.t = t;
md5 = new byte[16];
Log.d("MyCP", "build from raw packet: "+Utils.INSTANCE.toHexStr(raw));
System.arraycopy(raw, 2, md5, 0, 16);
Log.d("MyCP", "md5: "+Utils.INSTANCE.toHexStr(md5));
data = new byte[raw.length-1-1-16];
System.arraycopy(raw, 1+1+16, data, 0, data.length);
Log.d("MyCP", "data length: "+data.length);
}
public @NonNull byte[] encrypt(byte seq) {
byte[] dat = t.encryptLittleEndian(data, seq);
byte[] d = new byte[1+1+16+dat.length];
d[0] = cmd;
d[1] = (byte) dat.length;
System.arraycopy(md5, 0, d, 2, 16);
System.arraycopy(dat, 0, d, 1+1+16, dat.length);
return d;
}
public byte[] decrypt(byte seq) throws NoSuchAlgorithmException {
byte[] dat = t.decryptLittleEndian(data, seq);
if (dat != null && Arrays.equals(MessageDigest.getInstance("MD5").digest(dat), md5)) {
return dat;
}
return null;
}
public final static byte CMDGET = 0, CMDCAT = 1, CMDMD5 = 2, CMDACK = 3, CMDEND = 4, CMDSET = 5, CMDDEL = 6, CMDDAT = 7;
}

View File

@@ -23,17 +23,31 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.view.children
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.lapism.search.internal.SearchLayout
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main.view.*
import kotlinx.android.synthetic.main.activity_main.cctrl
import kotlinx.android.synthetic.main.activity_main.ffms
import kotlinx.android.synthetic.main.activity_main.ffsw
import kotlinx.android.synthetic.main.card_bottom.cbcard
import kotlinx.android.synthetic.main.dialog_input.view.*
import kotlinx.android.synthetic.main.dialog_input.view.diet
import kotlinx.android.synthetic.main.dialog_input.view.dis
import kotlinx.android.synthetic.main.dialog_input.view.dit
import kotlinx.android.synthetic.main.fragment_main.fmvp
import kotlinx.android.synthetic.main.line_bottom.view.*
import kotlinx.android.synthetic.main.line_word.view.*
import kotlinx.android.synthetic.main.line_bottom.view.lbtindex
import kotlinx.android.synthetic.main.line_bottom.view.lbttotal
import kotlinx.android.synthetic.main.line_bottom.view.sb
import kotlinx.android.synthetic.main.line_word.view.ta
import kotlinx.android.synthetic.main.line_word.view.tb
import kotlinx.android.synthetic.main.line_word.view.tn
import kotlinx.android.synthetic.main.line_word.view.vl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.fumiama.sdict.io.Client
import top.fumiama.sdict.SimpleDict
import java.io.FileNotFoundException
class MainActivity : AppCompatActivity() {
@@ -42,7 +56,7 @@ class MainActivity : AppCompatActivity() {
private var port = 80
private var pwd = "demo"
private var spwd: String? = null
private var dict: SimpleDict? = null
private val dict: SimpleDict by lazy { SimpleDict(Client(host, port), pwd, externalCacheDir, spwd) }
private var cm: ClipboardManager? = null
private var noShowNisi = false
private var mViewPagerPosition = 0
@@ -61,8 +75,7 @@ class MainActivity : AppCompatActivity() {
if(contains("spwd")) getString("spwd", spwd)?.apply { spwd = this }
if(contains("noNisi")) getBoolean("noNisi", noShowNisi).apply { noShowNisi = this }
}
Log.d("MyMain", "noNisi: $noShowNisi")
dict = SimpleDict(Client(host, port), pwd, externalCacheDir, spwd)
Log.d("MyMain", "server: $host:$port, noNisi: $noShowNisi")
cm = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@@ -77,13 +90,17 @@ class MainActivity : AppCompatActivity() {
ffsw.apply {
setOnRefreshListener {
fetchThread {
updateSize()
lifecycleScope.launch {
fetch {
updateSize()
}
}
}
isRefreshing = true
fetchThread {
updateSize()
lifecycleScope.launch {
fetch {
updateSize()
}
}
}
@@ -98,8 +115,10 @@ class MainActivity : AppCompatActivity() {
val a = lm.findFirstVisibleItemPosition()
val b = lm.findLastVisibleItemPosition()
val total = lm.itemCount
if(a <= 0) adapter.scrollUp(1)
else if(b >= total-1) adapter.scrollDown(1)
lifecycleScope.launch {
if(a <= 0) adapter.scrollUp(1)
else if(b >= total-1) adapter.scrollDown(1)
}
}
})
setAdapter(adapter)
@@ -130,8 +149,7 @@ class MainActivity : AppCompatActivity() {
override fun onQueryTextSubmit(query: CharSequence): Boolean {
if(query.isNotEmpty()) {
val key = query.toString()
val data = dict?.get(key)
showDictAlert(key, data, recyclerView.children.toList().let { children ->
showDictAlert(key, dict[key], recyclerView.children.toList().let { children ->
val i = children.map { it.ta.text }.indexOf(key)
if(i >= 0) children[i] else null
})
@@ -207,7 +225,7 @@ class MainActivity : AppCompatActivity() {
if(isSeeking) {
val bar = mControlBarStates[mViewPagerPosition]
bar.index = bar.getPosition(p)
updateSize(false)
lifecycleScope.launch { updateSize(false) }
}
}
@@ -221,7 +239,7 @@ class MainActivity : AppCompatActivity() {
Log.d("MyMain", "onStopTrackingTouch")
s?.progress?.let {
val ad = mVPAdapter.views[mViewPagerPosition]?.recyclerView?.adapter as? ListViewHolder.RecyclerViewAdapter ?: return
ad.setProgress(it)
lifecycleScope.launch { ad.setProgress(it) }
}
}
})
@@ -252,7 +270,7 @@ class MainActivity : AppCompatActivity() {
}
}
private fun updateSize(updateSeekbar: Boolean = true) = runOnUiThread {
private suspend fun updateSize(updateSeekbar: Boolean = true) = withContext(Dispatchers.Main) {
Log.d("MyMain", "update size, updateSeekbar: $updateSeekbar")
val bar = mControlBarStates[mViewPagerPosition]
cctrl?.lbtindex?.text = bar.formatRange(getString(R.string.info_index_meter))
@@ -260,25 +278,25 @@ class MainActivity : AppCompatActivity() {
if (updateSeekbar) cctrl?.sb?.progress = bar.getPercentage()
}
private fun fetchThread(doWhenFinish: (()->Unit)? = null) {
Thread{
dict?.fetchDict({
runOnUiThread {
private suspend fun fetch(doWhenFinish: (suspend ()->Unit)? = null) {
withContext(Dispatchers.IO) {
dict.fetch({
withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, R.string.toast_refresh_failed, Toast.LENGTH_SHORT).show()
}
}, {
runOnUiThread {
withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, R.string.toast_refresh_succeeded, Toast.LENGTH_SHORT).show()
}
}) {
runOnUiThread {
withContext(Dispatchers.Main) {
ffsw.isRefreshing = false
(mVPAdapter.views[mViewPagerPosition]?.recyclerView?.adapter as? ListViewHolder.RecyclerViewAdapter)?.refresh()
updateSize()
doWhenFinish?.apply { this() }
doWhenFinish?.invoke()
}
}
}.start()
}
}
private fun showDictAlert(key: String, data: String?, line: View?) {
@@ -296,34 +314,40 @@ class MainActivity : AppCompatActivity() {
.setView(t)
.setPositiveButton(android.R.string.ok) { _, _ ->
val newText = t.diet.text.toString().trim().replace(Regex("[\\uFF00-\\uFF5E]")) { (it.value[0] - 0xFEE0).toString() }
if (t.diet.text.isNotEmpty() && newText != data) Thread {
val k = key.trim().replace(Regex("[\\uFF00-\\uFF5E]")) { (it.value[0] - 0xFEE0).toString() }
if(dict?.set(k, newText) == true) {
line?.tb?.text = newText
} else runOnUiThread {
Toast.makeText(this, R.string.toast_failed, Toast.LENGTH_SHORT).show()
if (t.diet.text.isNotEmpty() && newText != data) lifecycleScope.launch {
withContext(Dispatchers.IO) {
val k = key.trim().replace(Regex("[\\uFF00-\\uFF5E]")) { (it.value[0] - 0xFEE0).toString() }
if(dict.set(k, newText)) withContext(Dispatchers.Main) {
line?.tb?.text = newText
} else withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, R.string.toast_failed, Toast.LENGTH_SHORT).show()
}
}
}.start()
}
else Toast.makeText(this, R.string.toast_unchanged, Toast.LENGTH_SHORT).show()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
.setNeutralButton(R.string.alert_word_button_delete) { _, _ ->
Thread{
if(dict?.del(key) == true) line?.apply {
val delKey = SpannableString(key)
val delData = SpannableString(data)
delKey.setSpan(StrikethroughSpan(), 0, key.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
delData.setSpan(StrikethroughSpan(), 0, (data?.length?:0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
ta.text = delKey
tn.text = delKey
tb.text = delData
lifecycleScope.launch{
withContext(Dispatchers.IO) {
if(dict.del(key)) line?.apply {
val delKey = SpannableString(key)
val delData = SpannableString(data)
delKey.setSpan(StrikethroughSpan(), 0, key.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
delData.setSpan(StrikethroughSpan(), 0, (data?.length?:0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
withContext(Dispatchers.Main) {
ta.text = delKey
tn.text = delKey
tb.text = delData
}
}
else withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, R.string.toast_failed, Toast.LENGTH_SHORT).show()
}
}
else runOnUiThread {
Toast.makeText(this, R.string.toast_failed, Toast.LENGTH_SHORT).show()
}
}.start()
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
@@ -354,21 +378,21 @@ class MainActivity : AppCompatActivity() {
inner class SearchViewHolder(itemView: View) : ListViewHolder(itemView) {
inner class RecyclerViewAdapter : ListViewHolder.RecyclerViewAdapter(visibleThreshold) {
override fun getKeys(filterText: CharSequence?) = filterText?.let { filter(it) }
override fun getValue(key: String) = dict?.get(key)
override fun getValue(key: String) = dict[key]
private fun filter(text: CharSequence): List<String> {
return dict?.keys?.filter {
it.contains(text, true)
}?.toSet()?.plus(
dict?.filterValues {
return dict.keys.filter {
it.contains(text, true)
}.toSet().plus(
dict.filterValues {
it?.contains(text, true) ?: false
}.let {
val newSet = mutableSetOf<String>()
it?.keys?.forEach { k ->
it.keys.forEach { k ->
newSet += k
}
newSet
}
)?.toList()?: emptyList()
).toList()
}
}
}
@@ -383,7 +407,7 @@ class MainActivity : AppCompatActivity() {
bar.sort(keys.toList())
}
}
else dict?.latestKeys?.let { keys ->
else dict.latestKeys.let { keys ->
Log.d("MyMain", "LikeViewHolder getKeys all, set size: ${keys.size}")
mControlBarStates[0].let { bar ->
bar.total = keys.size
@@ -391,7 +415,7 @@ class MainActivity : AppCompatActivity() {
}
}
)?: emptyList()
override fun getValue(key: String) = dict?.get(key)?:dictPreferences?.getString(key, "null")?:"N/A"
override fun getValue(key: String) = dict[key] ?:dictPreferences?.getString(key, "null")?:"N/A"
}
}
@@ -417,15 +441,15 @@ class MainActivity : AppCompatActivity() {
override fun onBindViewHolder(holder: ListViewHolder, p: Int) {
val position = p + index
Log.d("MyMain", "Bind open at $p($position)")
Thread{
lifecycleScope.launch { withContext(Dispatchers.IO) {
listKeys?.apply {
if (position >= size) return@Thread
if (position >= size) return@withContext
val key = get(position)
val data = getValue(key)
val like = dictPreferences?.contains(key) == true
//Log.d("MyMain", "Like status of $key is $like")
holder.itemView.apply {
runOnUiThread {
holder.itemView.apply line@ {
withContext(Dispatchers.Main) {
if (!noShowNisi) {
tn.visibility = View.VISIBLE
tn.text = key
@@ -435,13 +459,11 @@ class MainActivity : AppCompatActivity() {
vl.setBackgroundResource(if(like) R.drawable.ic_like_filled else R.drawable.ic_like)
//Log.d("MyMain", "Set like of $key: $like")
setOnClickListener {
showDictAlert(key, data, this)
showDictAlert(key, data, this@line)
}
setOnLongClickListener {
cm?.setPrimaryClip(ClipData.newPlainText("SimpleDict", "$key\n$data"))
runOnUiThread {
Toast.makeText(this@MainActivity, R.string.toast_copied, Toast.LENGTH_SHORT).show()
}
Toast.makeText(this@MainActivity, R.string.toast_copied, Toast.LENGTH_SHORT).show()
true
}
vl.setOnClickListener {
@@ -464,7 +486,7 @@ class MainActivity : AppCompatActivity() {
if(p >= itemCount-1) scrollDown(if(p < renderLinesCount) 4 else 1)
else if(p <= 1) scrollUp(if(p < renderLinesCount) 4 else 1)
}
}.start()
} }
}
override fun getItemCount() = (listKeys?.size?:0).let { if(it > renderLinesCount) renderLinesCount else it }
@@ -478,7 +500,7 @@ class MainActivity : AppCompatActivity() {
}
@SuppressLint("NotifyDataSetChanged")
fun scrollDown(n: Int) {
suspend fun scrollDown(n: Int) {
if((listKeys?.size ?: 0) <= renderLinesCount) return
val oldIndex = index
val nextIndex = if(oldIndex + n + renderLinesCount > (listKeys?.size ?: 0)) (listKeys?.size ?: 0) - renderLinesCount else oldIndex + n
@@ -486,7 +508,7 @@ class MainActivity : AppCompatActivity() {
if(nextIndex < 0) return
index = nextIndex
if(n >= renderLinesCount) {
runOnUiThread { notifyDataSetChanged() }
withContext(Dispatchers.Main) { notifyDataSetChanged() }
return
}
// index next index
@@ -495,25 +517,25 @@ class MainActivity : AppCompatActivity() {
// ---remain--- ↑
// ----delete---- → → → → → ↗
val insert = nextIndex - oldIndex
runOnUiThread {
withContext(Dispatchers.Main) {
notifyItemRangeInserted(renderLinesCount, insert)
notifyItemRangeRemoved(0, insert)
}
}
@SuppressLint("NotifyDataSetChanged")
fun scrollUp(n: Int) {
suspend fun scrollUp(n: Int) {
if((listKeys?.size ?: 0) <= renderLinesCount) return
val oldIndex = index
val nextIndex = if(oldIndex-n >= 0) oldIndex-n else 0
if(oldIndex == nextIndex) return
index = nextIndex
if(n >= renderLinesCount) {
runOnUiThread { notifyDataSetChanged() }
withContext(Dispatchers.Main) { notifyDataSetChanged() }
return
}
val insert = oldIndex - nextIndex
runOnUiThread {
withContext(Dispatchers.Main) {
notifyItemRangeInserted(0, insert)
notifyItemRangeRemoved(renderLinesCount, insert)
}
@@ -522,7 +544,7 @@ class MainActivity : AppCompatActivity() {
fun getPosition() = index
@SuppressLint("NotifyDataSetChanged")
fun setProgress(p: Int) {
suspend fun setProgress(p: Int) {
if(p > 100 || p < 0) return
var newIndex = p * (listKeys?.size?:0) / 100
if(newIndex + renderLinesCount > (listKeys?.size?:0)) {
@@ -534,7 +556,7 @@ class MainActivity : AppCompatActivity() {
val n = newIndex - oldIndex
if(n >= renderLinesCount || n <= -renderLinesCount) {
index = newIndex
runOnUiThread { notifyDataSetChanged() }
withContext(Dispatchers.Main) { notifyDataSetChanged() }
return
}
if(n > 0) scrollDown(n)
@@ -550,7 +572,7 @@ class MainActivity : AppCompatActivity() {
if(ad?.hasRefreshed == false) {
ad.refresh()
}
updateSize()
lifecycleScope.launch { updateSize() }
}
override fun onPageScrollStateChanged(state: Int) {
@@ -583,7 +605,7 @@ class MainActivity : AppCompatActivity() {
Log.d("MyMain", "new start: $newStart, index: ${bar.index}")
if (newStart != bar.index) {
bar.index = newStart
updateSize()
lifecycleScope.launch { updateSize() }
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
@@ -593,8 +615,10 @@ class MainActivity : AppCompatActivity() {
Log.d("MyMain", "new scroll state: $newState, a: $a, b: $b")
this@MainActivity.ffsw.isEnabled = newState == 0 && a == 0
val total = lm.itemCount
if(a <= 0) ad.scrollUp(1)
else if(b >= total-1) ad.scrollDown(1)
lifecycleScope.launch {
if(a <= 0) ad.scrollUp(1)
else if(b >= total-1) ad.scrollDown(1)
}
}
})
}

View File

@@ -1,170 +0,0 @@
package top.fumiama.simpledict
import android.util.Log
import java.io.File
import java.lang.Thread.sleep
import java.security.MessageDigest
class SimpleDict(private val client: Client, pwd: String, private val externalCacheDir: File?, spwd: String?) { //must run in thread
private var dict = HashMap<String, String?>()
val size get() = dict.size
val keys get() = dict.keys
var latestKeys = arrayOf<String>()
private var seq: Byte = 0
private val ptea = Tea(pwd.toByteArray())
private val stea = spwd?.let { Tea(it.toByteArray()) }
private val md5File = File(externalCacheDir, "md5")
private val dspFile = File(externalCacheDir, "dsp")
private val filler = "fill".toByteArray()
private val raw: ByteArray?
get() {
var times = 3
var re: ByteArray? = null
var exit = false
while(times-- > 0 && !exit) {
if(initDict()) {
client.sendMessage(CmdPacket(CmdPacket.CMDCAT, filler, ptea).encrypt(seq))
try {
var length = ""
var c = client.read()
while (c?.isDigit() == true) {
length += c
c = client.read()
}
Log.d("MySD", "length: $length")
re = ptea.decryptLittleEndian(client.receiveRawMessage(length.toInt()), (seq+1).toByte())
if(re != null) seq = (seq + 2).toByte()
exit = true
} catch (e: Exception){
e.printStackTrace()
}
closeDict()
} else sleep(233)
}
return re
}
private val ack: ByteArray?
get() {
var re = client.receiveRawMessage(1+1+16)
re += client.receiveRawMessage(re[1].toInt())
val r = CmdPacket(re, ptea).decrypt(seq)
if (r != null) seq++
Log.d("MySD", "ack: ${r?.decodeToString()}")
return r
}
private fun initDict() = client.initConnect()
private fun closeDict(): Boolean {
client.sendMessage(CmdPacket(CmdPacket.CMDEND, filler, ptea).encrypt(seq))
seq = 0
return client.closeConnect()
}
private fun saveDict(data: ByteArray) {
if(externalCacheDir?.exists() != true) externalCacheDir?.mkdirs()
if(externalCacheDir?.exists() == true) {
dspFile.writeBytes(data)
md5File.writeBytes(MessageDigest.getInstance("md5").digest(data))
}
}
private fun hasNewItem(md5: ByteArray): Boolean =
if(initDict()) {
client.sendMessage(CmdPacket(CmdPacket.CMDMD5, md5, ptea).encrypt(seq++))
val cp = ack
Log.d("MySD", "Check md5: ${cp?.decodeToString()}")
closeDict()
cp?.decodeToString() == "nequ"
} else false
private fun analyzeDict(datas: ByteArray, saveDict: Boolean) {
SimpleProtobuf.getDictArray(datas).forEach { d ->
d?.apply {
val k = key.decodeToString()
if(saveDict) {
if(k.toByteArray().contentEquals(key)) {
dict[k] = data.decodeToString()
latestKeys += k
} else {
sendel(key) // 去错
}
} else if(!dict.containsKey(k)){
dict[k] = data.decodeToString()
latestKeys += k
} else {
sendel(key) // 去重
}
}
}
if(saveDict) saveDict(datas)
}
fun filterValues(predicate: (String?) -> Boolean) = dict.filterValues(predicate)
fun fetchDict(doOnLoadFailure: ()->Unit, doOnLoadSuccess: ()->Unit, doCommon: (() -> Unit)? = null) {
val noChange = md5File.exists() && dspFile.exists() && !hasNewItem(md5File.readBytes())
val data = if(noChange) dspFile.readBytes() else raw
dict.clear()
latestKeys = arrayOf()
if(data == null) doOnLoadFailure()
else {
analyzeDict(data, !noChange)
doOnLoadSuccess()
}
doCommon?.let { it() }
}
fun del(key: String): Boolean {
if(stea == null) return false
else if(initDict()) {
client.sendMessage(CmdPacket(CmdPacket.CMDDEL, key.toByteArray(), stea).encrypt(seq++))
if(ack?.decodeToString() == "succ") {
if(closeDict()) {
dict.remove(key)
val end = latestKeys.size-1
if(end > 0) latestKeys = latestKeys.let { oldArr ->
var index = -1
Array(end) {
if(oldArr[it] == key) index = it
return@Array if(index < 0 || (index > 0 && it < index)) oldArr[it] else oldArr[it+1]
}
}
return true
}
} else closeDict()
}
return false
}
private fun sendel(key: ByteArray): Boolean {
if(stea == null) return false
else if(initDict()) {
client.sendMessage(CmdPacket(CmdPacket.CMDDEL, key, stea).encrypt(seq++))
if(ack?.decodeToString() == "succ") {
return closeDict()
} else closeDict()
}
return false
}
operator fun get(key: String) = dict[key]
fun set(key: String, value: String): Boolean {
//if(spwd == null) return false
if(stea == null) return false
val contain = dict.containsKey(key)
if((contain && sendel(key.toByteArray())) || !contain) {
if(initDict()) {
client.sendMessage(CmdPacket(CmdPacket.CMDSET, key.toByteArray(), stea).encrypt(seq++))
if(ack?.decodeToString() == "data") {
client.sendMessage(CmdPacket(CmdPacket.CMDDAT, value.toByteArray(), stea).encrypt(seq++))
val s = ack?.decodeToString() == "succ"
if(s) dict[key] = value
return closeDict() && s
} else closeDict()
}
return false
} else return false
}
}

View File

@@ -1,81 +0,0 @@
package top.fumiama.simpledict;
import org.jetbrains.annotations.NotNull;
import java.util.Stack;
public class SimpleProtobuf {
public static class Dict {
public byte[] key;
public byte[] data;
}
private static final DictStack ds = new DictStack();
public static Dict[] getDictArray(@NotNull byte[] raw) {
int offset = 0;
SLLE s;
while (offset < raw.length) {
offset += getSLLE(raw, offset).len; //struct_len
offset += getSLLE(raw, offset).len; //type
s = getSLLE(raw, offset); //data len
//Log.d("MySPB", "Data len:" + s.value);
Dict d = new Dict();
d.key = new byte[s.value];
offset += s.len;
System.arraycopy(raw, offset, d.key, 0, s.value);
offset += s.value;
offset += getSLLE(raw, offset).len; //type
s = getSLLE(raw, offset); //data len
//Log.d("MySPB", "Data len:" + s.value);
d.data = new byte[s.value];
offset += s.len;
System.arraycopy(raw, offset, d.data, 0, s.value);
offset += s.value;
ds.push(d);
}
return ds.popAllData();
}
@NotNull
private static SLLE getSLLE(byte[] p, int start) {
SLLE s = new SLLE();
s.value = 0;
for (int i = 0; i < 4; i++) {
s.value += (p[start + i] & 0x7f) << (i * 7);
if ((p[start + i] & 0x80) == 0) { //无更高位
s.len = i + 1;
break;
}
}
return s;
}
private static class SLLE {
int value;
int len;
}
private static class DictStack extends PopAllStack<Dict> {
public Dict[] popAllData() {
Object[] t = popAll();
if (t != null) {
Dict[] d = new Dict[t.length];
for (int i = 0; i < t.length; i++) {
d[i] = (Dict) t[i];
}
return d;
} else return null;
}
}
private static class PopAllStack<T> extends Stack<T> {
public Object[] popAll() {
if (size() > 0) {
Object[] t = new Object[size()];
System.arraycopy(elementData, 0, t, 0, size());
setSize(0);
return t;
} else return null;
}
}
}

View File

@@ -1,126 +0,0 @@
package top.fumiama.simpledict;
import android.util.Log;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Random;
public class Tea {
private final int[] t = new int[4];
private final Random r;
public Tea(@NonNull byte[] tea) {
byte[] tea16 = new byte[16];
System.arraycopy(tea, 0, tea16, 0, Math.min(tea.length, 15));
tea16[15] = 0;
ByteBuffer bf = ByteBuffer.wrap(tea16).order(ByteOrder.LITTLE_ENDIAN);
t[0] = bf.getInt(0);
t[1] = bf.getInt(4);
t[2] = bf.getInt(8);
t[3] = bf.getInt(12) & 0x00ffffff;
r = new Random();
//Log.d("MyTEA", "t: "+ Arrays.toString(t));
}
public @NonNull byte[] encryptLittleEndian(@NonNull byte[] src, byte seq) {
int lens = src.length;
int fill = 10 - (lens+1)%8;
int dstlen = fill+lens+7;
byte[] dst = new byte[dstlen];
byte[] randfill = new byte[fill-1];
t[3] = ((int)seq)<<24 | (t[3]&0x00ffffff);
Log.d("MyTEA", "encrypt seq: "+ seq);
r.nextBytes(randfill);
//Log.d("MyTEA", "rand fill: "+ Utils.INSTANCE.toHexStr(randfill));
System.arraycopy(randfill, 0, dst, 1, fill-1);
dst[0] = (byte)((fill-3)|0xF8); // 存储pad长度
System.arraycopy(src, 0, dst, fill, lens);
//Log.d("MyTEA", "dst before enc: "+Utils.INSTANCE.toHexStr(dst));
long iv1 = 0, iv2 = 0, holder;
ByteBuffer bf = ByteBuffer.wrap(dst).order(ByteOrder.LITTLE_ENDIAN);
for(int i = 0; i < dstlen; i += 8) {
long block = bf.getLong(i);
holder = block ^ iv1;
int v0 = (int)(holder>>32);
int v1 = (int)holder;
for (int j = 0; j < 0x10; j++) {
v0 += (v1 + sumtable[j]) ^ ((int)(((long) v1 << 4)&0x00000000fffffff0L) + t[0]) ^ ((int)((v1 >> 5)&0x07ffffff) + t[1]);
v1 += (v0 + sumtable[j]) ^ ((int)(((long) v0 << 4)&0x00000000fffffff0L) + t[2]) ^ ((int)((v0 >> 5)&0x07ffffff) + t[3]);
}
//Log.d("MyTEA", "v0: "+Integer.toHexString(v0)+", v1: "+Integer.toHexString(v1));
iv1 = (((long)v0)<<32) | (((long)v1)&0x00000000ffffffffL);
//Log.d("MyTEA", "iv1: "+Long.toHexString(iv1));
iv1 = iv1 ^ iv2;
iv2 = holder;
//Log.d("MyTEA", "put: "+Long.toHexString(iv1));
bf.putLong(i, iv1);
}
//Log.d("MyTEA", "dst after enc: "+Utils.INSTANCE.toHexStr(dst));
return dst;
}
public byte[] decryptLittleEndian(@NonNull byte[] src, byte seq) {
if (src.length < 16 || (src.length)%8 != 0) {
return null;
}
byte[] dst = new byte[src.length];
long iv1, iv2 = 0, holder = 0;
t[3] = ((int)seq)<<24 | (t[3]&0x00ffffff);
Log.d("MyTEA", "decrypt seq: "+ seq);
ByteBuffer sbf = ByteBuffer.wrap(src).order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer dbf = ByteBuffer.wrap(dst).order(ByteOrder.LITTLE_ENDIAN);
for(int i = 0; i < src.length; i += 8) {
iv1 = sbf.getLong(i);
iv2 ^= iv1;
int v0 = (int)(iv2>>32);
int v1 = (int)iv2;
for (int j = 0x0f; j >= 0; j--) {
v1 -= (v0 + sumtable[j]) ^ ((int)(((long) v0 << 4)&0x00000000fffffff0L) + t[2]) ^ ((int)((v0 >> 5)&0x07ffffff) + t[3]);
v0 -= (v1 + sumtable[j]) ^ ((int)(((long) v1 << 4)&0x00000000fffffff0L) + t[0]) ^ ((int)((v1 >> 5)&0x07ffffff) + t[1]);
}
iv2 = (((long)v0)<<32) | (((long)v1)&0x00000000ffffffffL);
dbf.putLong(i, iv2^holder);
holder = iv1;
}
int start = (dst[0]&7)+3;
Log.d("MyTEA", "decrypt start: "+ start);
int datlen = src.length-7-start;
if(datlen <= 0) return null;
byte[] dat = new byte[datlen];
Log.d("MyTEA", "decrypt data length: "+datlen);
System.arraycopy(dst, start, dat, 0, datlen);
return dat;
}
// TEA encoding sumtable
private static final int[] sumtable = {
0x9e3579b9,
0x3c6ef172,
0xd2a66d2b,
0x78dd36e4,
0x17e5609d,
0xb54fda56,
0x5384560f,
0xf1bb77c8,
0x8ff24781,
0x2e4ac13a,
0xcc653af3,
0x6a9964ac,
0x08d12965,
0xa708081e,
0x451221d7,
0xe37793d0,
};
}

View File

@@ -1,14 +0,0 @@
package top.fumiama.simpledict
object Utils {
fun toHexStr(byteArray: ByteArray) =
with(StringBuilder()) {
byteArray.forEach {
val hex = it.toInt() and (0xFF)
val hexStr = Integer.toHexString(hex)
if (hexStr.length == 1) append("0").append(hexStr)
else append(hexStr)
}
toString()
}
}

View File

@@ -1,17 +0,0 @@
package top.fumiama.simpledict
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}