Minor Refactor

This commit is contained in:
tarsin committed 2022-12-18 12:52:30 +08:00
1 parent bd2d54f774
commit 8b94997cde
6 files changed
+116 -153

No files matched your search

@@ -43,6 +43,7 @@ import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhX509TrustManager
import com.hippo.ehviewer.client.data.GalleryDetail
@@ -114,6 +115,7 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
EhDB.initialize(this)
EhEngine.initialize()
BitmapUtils.initialize(this)
EhTagDatabase.update()
// Locales can be managed by system automatically above Snow Cone v2
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
@@ -16,17 +16,20 @@
package com.hippo.ehviewer.client
import android.content.Context
import androidx.core.util.Pair
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.R
import com.hippo.util.ExceptionUtils
import com.hippo.util.HashCodeUtils
import com.hippo.util.IoThreadPoolExecutor
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IOUtils
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.BufferedSource
@@ -41,38 +44,36 @@ import java.io.IOException
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
class EhTagDatabase(private val name: String, source: BufferedSource) {
private val tags: JSONObject?
private var tagList: ArrayList<Pair<String, String>>? = null
object EhTagDatabase {
private lateinit var tags: JSONObject
private lateinit var tagList: ArrayList<Pair<String, String>>
init {
var tmpTags: JSONObject? = null
fun isInitialized(): Boolean {
return this::tags.isInitialized && this::tagList.isInitialized
}
private fun updateData(source: BufferedSource) {
try {
tmpTags = JSONObject(source.readString(StandardCharsets.UTF_8))
} catch (e: JSONException) {
e.printStackTrace()
}
tags = tmpTags
if (tags != null) {
tagList = ArrayList()
tags.keys().forEachRemaining { prefix: String ->
val values = tags.optJSONObject(prefix)
val tmpTags = JSONObject(source.readString(StandardCharsets.UTF_8))
val tmpTagList = arrayListOf<Pair<String, String>>()
tmpTags.keys().forEachRemaining { prefix: String ->
val values = tmpTags.optJSONObject(prefix)
values?.let {
it.keys().forEachRemaining { tag: String ->
tagList!!.add(Pair("$prefix:$tag", values.optString(tag)))
tmpTagList.add(Pair("$prefix:$tag", values.optString(tag)))
}
}
}
} else {
tagList = null
tags = tmpTags
tagList = tmpTagList
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun getTranslation(prefix: String?, tag: String?): String? {
return tags!!.optJSONObject(prefix)?.optString(tag)?.trim()?.takeIf { it.isNotEmpty() }
return tags.optJSONObject(prefix)?.optString(tag)?.trim()?.takeIf { it.isNotEmpty() }
}
/* Construct a cold flow for tag database suggestions */
@@ -81,7 +82,7 @@ class EhTagDatabase(private val name: String, source: BufferedSource) {
translate: Boolean,
exactly: Boolean = false
): Flow<Pair<String?, String>> = flow {
tagList?.forEach {
tagList.forEach {
val tag = it.first
val hint = if (translate) it.second else null
val index = tag.indexOf(':')
@@ -108,126 +109,102 @@ class EhTagDatabase(private val name: String, source: BufferedSource) {
return text != null && text.replace(" ", "").contains(key.replace(" ", ""))
}
companion object {
private val NAMESPACE_TO_PREFIX: MutableMap<String, String> = HashMap()
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
it["artist"] = "a"
it["cosplayer"] = "cos"
it["character"] = "c"
it["female"] = "f"
it["group"] = "g"
it["language"] = "l"
it["male"] = "m"
it["mixed"] = "x"
it["other"] = "o"
it["parody"] = "p"
it["reclass"] = "r"
}
// TODO more lock for different language
private val lock: Lock = ReentrantLock()
@JvmStatic
fun namespaceToPrefix(namespace: String): String? {
return NAMESPACE_TO_PREFIX[namespace]
}
@JvmStatic
@Volatile
var instance: EhTagDatabase? = null
private set
private fun getMetadata(context: Context): Array<String>? {
return context.resources.getStringArray(R.array.tag_translation_metadata)
.takeIf { it.size == 4 }
}
init {
NAMESPACE_TO_PREFIX["artist"] = "a"
NAMESPACE_TO_PREFIX["cosplayer"] = "cos"
NAMESPACE_TO_PREFIX["character"] = "c"
NAMESPACE_TO_PREFIX["female"] = "f"
NAMESPACE_TO_PREFIX["group"] = "g"
NAMESPACE_TO_PREFIX["language"] = "l"
NAMESPACE_TO_PREFIX["male"] = "m"
NAMESPACE_TO_PREFIX["mixed"] = "x"
NAMESPACE_TO_PREFIX["other"] = "o"
NAMESPACE_TO_PREFIX["parody"] = "p"
NAMESPACE_TO_PREFIX["reclass"] = "r"
fun isTranslatable(): Boolean {
return EhApplication.application.resources.getBoolean(R.bool.tag_translatable)
}
private fun getFileContent(file: File): String? {
try {
file.source().buffer().use { return it.readString(StandardCharsets.UTF_8) }
} catch (e: IOException) {
return null
}
}
@JvmStatic
fun namespaceToPrefix(namespace: String): String? {
return NAMESPACE_TO_PREFIX[namespace]
}
private fun getMetadata(context: Context): Array<String>? {
val metadata = context.resources.getStringArray(R.array.tag_translation_metadata)
return if (metadata.size == 4) {
metadata
} else {
null
}
}
fun isTranslatable(context: Context): Boolean {
return context.resources.getBoolean(R.bool.tag_translatable)
}
private fun getFileContent(file: File): String? {
try {
file.source().buffer()
.use { source -> return source.readString(StandardCharsets.UTF_8) }
} catch (e: IOException) {
return null
}
}
private fun getFileSha1(file: File): String? {
try {
FileInputStream(file).use { `is` ->
val digest = MessageDigest.getInstance("SHA-1")
var n: Int
val buffer = ByteArray(4 * 1024)
while (`is`.read(buffer).also { n = it } != -1) {
digest.update(buffer, 0, n)
}
return HashCodeUtils.bytesToHexString(digest.digest())
private fun getFileSha1(file: File): String? {
try {
FileInputStream(file).use { stream ->
val digest = MessageDigest.getInstance("SHA-1")
var n: Int
val buffer = ByteArray(4 * 1024)
while (stream.read(buffer).also { n = it } != -1) {
digest.update(buffer, 0, n)
}
} catch (e: IOException) {
return null
} catch (e: NoSuchAlgorithmException) {
return null
return HashCodeUtils.bytesToHexString(digest.digest())
}
} catch (e: IOException) {
return null
} catch (e: NoSuchAlgorithmException) {
return null
}
}
private fun checkData(sha1File: File, dataFile: File): Boolean {
val s1 = getFileContent(sha1File) ?: return false
val s2 = getFileSha1(dataFile) ?: return false
return s1 == s2
}
private fun checkData(sha1File: File, dataFile: File): Boolean {
val s1 = getFileContent(sha1File) ?: return false
val s2 = getFileSha1(dataFile) ?: return false
return s1 == s2
}
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
val request: Request = Request.Builder().url(url).build()
val call = client.newCall(request)
try {
call.execute().use { response ->
if (!response.isSuccessful) {
return false
}
val body = response.body
body.byteStream()
.use { `is` -> FileOutputStream(file).use { os -> IOUtils.copy(`is`, os) } }
return true
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
val request: Request = Request.Builder().url(url).build()
val call = client.newCall(request)
try {
call.execute().use { response ->
if (!response.isSuccessful) {
return false
}
} catch (t: Throwable) {
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
return false
val body = response.body
body.byteStream()
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
return true
}
} catch (t: Throwable) {
FileUtils.delete(file)
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
return false
}
}
@JvmStatic
fun update(context: Context) {
val urls = getMetadata(context)
if (urls == null || urls.size != 4) {
// Clear tags if it's not possible
instance = null
return
}
@OptIn(DelicateCoroutinesApi::class)
@Synchronized
@JvmStatic
fun update() {
val urls = getMetadata(EhApplication.application)
urls?.let {
val sha1Name = urls[0]
val sha1Url = urls[1]
val dataName = urls[2]
val dataUrl = urls[3]
// Clear tags if name if different
val tmp = instance
if (tmp != null && tmp.name != dataName) {
instance = null
}
IoThreadPoolExecutor.getInstance().execute {
if (!lock.tryLock()) {
return@execute
}
GlobalScope.launch(Dispatchers.IO) {
try {
val dir = AppConfig.getFilesDir("tag-translations") ?: return@execute
val dir = AppConfig.getFilesDir("tag-translations")
checkNotNull(dir)
// Check current sha1 and current data
val sha1File = File(dir, sha1Name)
@@ -238,10 +215,9 @@ class EhTagDatabase(private val name: String, source: BufferedSource) {
}
// Read current EhTagDatabase
if (instance == null && dataFile.exists()) {
if (!isInitialized() && dataFile.exists()) {
try {
dataFile.source().buffer()
.use { source -> instance = EhTagDatabase(dataName, source) }
dataFile.source().buffer().use { updateData(it) }
} catch (e: IOException) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
@@ -251,30 +227,24 @@ class EhTagDatabase(private val name: String, source: BufferedSource) {
// Save new sha1
val tempSha1File = File(dir, "$sha1Name.tmp")
if (!save(client, sha1Url, tempSha1File)) {
FileUtils.delete(tempSha1File)
return@execute
}
check(save(client, sha1Url, tempSha1File))
// Check new sha1 and current data
if (checkData(tempSha1File, dataFile)) {
// The data is the same
FileUtils.delete(tempSha1File)
return@execute
return@launch
}
// Save new data
val tempDataFile = File(dir, "$dataName.tmp")
if (!save(client, dataUrl, tempDataFile)) {
FileUtils.delete(tempDataFile)
return@execute
}
check(save(client, dataUrl, tempDataFile))
// Check new sha1 and new data
if (!checkData(tempSha1File, tempDataFile)) {
FileUtils.delete(tempSha1File)
FileUtils.delete(tempDataFile)
return@execute
return@launch
}
// Replace current sha1 and current data with new sha1 and new data
@@ -285,13 +255,10 @@ class EhTagDatabase(private val name: String, source: BufferedSource) {
// Read new EhTagDatabase
try {
dataFile.source().buffer()
.use { source -> instance = EhTagDatabase(dataName, source) }
} catch (e: IOException) {
// Ignore
dataFile.source().buffer().use { updateData(it) }
} catch (_: IOException) {
}
} finally {
lock.unlock()
}
}
}
@@ -309,8 +309,6 @@ public final class MainActivity extends StageActivity
} else {
onRestore(savedInstanceState);
}
EhTagDatabase.update(this);
}
@@ -52,7 +52,7 @@ class EhFragment : BasePreferenceFragment() {
thumbResolution!!.setSummaryProvider {
getString(R.string.settings_eh_thumb_resolution_summary, (it as ListPreference).entry)
}
if (!EhTagDatabase.isTranslatable(requireActivity())) {
if (!EhTagDatabase.isTranslatable()) {
preferenceScreen.removePreference(showTagTranslations)
preferenceScreen.removePreference(tagTranslationsSource!!)
}
@@ -80,7 +80,7 @@ class EhFragment : BasePreferenceFragment() {
requireActivity().setResult(Activity.RESULT_OK)
} else if (Settings.KEY_SHOW_TAG_TRANSLATIONS == key) {
if (java.lang.Boolean.TRUE == newValue) {
EhTagDatabase.update(requireActivity())
EhTagDatabase.update()
}
} else if (Settings.KEY_BLACK_DARK_THEME == key) {
if (requireActivity().resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0) {
@@ -927,7 +927,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
mNoTags.setVisibility(View.GONE);
}
EhTagDatabase ehTags = Settings.getShowTagTranslations() ? EhTagDatabase.getInstance() : null;
EhTagDatabase ehTags = Settings.getShowTagTranslations() ? EhTagDatabase.INSTANCE : null;
int colorTag = ResourcesKt.resolveColor(getTheme(), R.attr.tagBackgroundColor);
int colorName = ResourcesKt.resolveColor(getTheme(), R.attr.tagGroupBackgroundColor);
for (GalleryTagGroup tg : tagGroups) {
@@ -936,7 +936,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
mTags.addView(ll);
String readableTagName = null;
if (ehTags != null) {
if (ehTags != null && ehTags.isInitialized()) {
readableTagName = ehTags.getTranslation("n", tg.groupName);
}
@@ -306,16 +306,12 @@ abstract class SearchBarScene : ToolbarScene() {
mSearchView?.editText?.text?.toString()?.let { text ->
mSuggestionProvider?.run { providerSuggestions(text)?.forEach { emit(it) } }
mSearchDatabase.getSuggestions(text, 128).forEach { emit(KeywordSuggestion(it)) }
EhTagDatabase.instance?.run {
EhTagDatabase.takeIf { it.isInitialized() }?.run {
if (!TextUtils.isEmpty(text) && !text.endsWith(" ")) {
val s = text.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (s.isNotEmpty()) {
val keyword = s[s.size - 1]
val translate =
Settings.getShowTagTranslations() && EhTagDatabase.isTranslatable(
requireContext()
)
val translate = Settings.getShowTagTranslations() && isTranslatable()
suggestFlow(keyword, translate, true).collect {
emit(TagSuggestion(it.first, it.second))
}