Skip to content

Commit 571e7c4

Browse files
committed
feat: smarter and faster assets syncing
Finally I remember to utilize the checksums pre-calculated on build, comparing the sha256 among the files to determine how to handle them. Ref: https://github.com/fcitx5-android/fcitx5-android/blob/59558c5b624359455911082b10750f4dcbd10fe8/app/src/main/java/org/fcitx/fcitx5/android/core/data
1 parent ff6b838 commit 571e7c4

File tree

6 files changed

+182
-62
lines changed

6 files changed

+182
-62
lines changed

app/src/main/java/com/osfans/trime/data/AppPrefs.kt

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,12 @@ class AppPrefs(
121121

122122
class Internal(private val prefs: AppPrefs) {
123123
companion object {
124-
const val LAST_VERSION_NAME = "general__last_version_name"
125124
const val PID = "general__pid"
126-
const val LAST_BUILD_GIT_HASH = "general__last_build_git_hash"
127125
}
128126

129-
var lastVersionName: String
130-
get() = prefs.getPref(LAST_VERSION_NAME, "")
131-
set(v) = prefs.setPref(LAST_VERSION_NAME, v)
132127
var pid: Int
133128
get() = prefs.getPref(PID, 0)
134129
set(v) = prefs.setPref(PID, v)
135-
var lastBuildGitHash: String
136-
get() = prefs.getPref(LAST_BUILD_GIT_HASH, "")
137-
set(v) = prefs.setPref(LAST_BUILD_GIT_HASH, v)
138130
}
139131

140132
/**
@@ -420,7 +412,6 @@ class AppPrefs(
420412
const val UI_MODE = "other__ui_mode"
421413
const val SHOW_APP_ICON = "other__show_app_icon"
422414
const val SHOW_STATUS_BAR_ICON = "other__show_status_bar_icon"
423-
const val DESTROY_ON_QUIT = "other__destroy_on_quit"
424415
}
425416

426417
var uiMode: String
@@ -432,8 +423,5 @@ class AppPrefs(
432423
var showStatusBarIcon: Boolean = false
433424
get() = prefs.getPref(SHOW_STATUS_BAR_ICON, false)
434425
private set
435-
var destroyOnQuit: Boolean = false
436-
get() = prefs.getPref(DESTROY_ON_QUIT, false)
437-
private set
438426
}
439427
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
package com.osfans.trime.data.base
6+
7+
import kotlinx.serialization.Serializable
8+
9+
@Serializable
10+
data class DataChecksums(
11+
val sha256: String,
12+
val files: Map<String, String>,
13+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
package com.osfans.trime.data.base
6+
7+
sealed interface DataDiff {
8+
val path: String
9+
10+
val ordinal: Int
11+
12+
data class CreateFile(override val path: String) : DataDiff {
13+
override val ordinal: Int
14+
get() = 3
15+
}
16+
17+
data class UpdateFile(override val path: String) : DataDiff {
18+
override val ordinal: Int
19+
get() = 2
20+
}
21+
22+
data class DeleteDir(override val path: String) : DataDiff {
23+
override val ordinal: Int
24+
get() = 1
25+
}
26+
27+
data class DeleteFile(override val path: String) : DataDiff {
28+
override val ordinal: Int
29+
get() = 0
30+
}
31+
32+
companion object {
33+
fun diff(
34+
old: DataChecksums,
35+
new: DataChecksums,
36+
): List<DataDiff> {
37+
if (old.sha256 == new.sha256) return emptyList()
38+
return new.files.mapNotNull { (path, sha256) ->
39+
when {
40+
path !in old.files && sha256.isNotBlank() -> CreateFile(path)
41+
old.files[path] != sha256 ->
42+
if (sha256.isNotBlank()) UpdateFile(path) else null
43+
else -> null
44+
}
45+
}.toMutableList().apply {
46+
addAll(
47+
old.files.filterKeys { it !in new.files }
48+
.map { (path, sha256) ->
49+
if (sha256.isNotBlank()) DeleteFile(path) else DeleteDir(path)
50+
},
51+
)
52+
}
53+
}
54+
}
55+
}

app/src/main/java/com/osfans/trime/data/base/DataManager.kt

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,50 @@
44

55
package com.osfans.trime.data.base
66

7+
import android.content.res.AssetManager
8+
import android.os.Build
79
import com.blankj.utilcode.util.PathUtils
8-
import com.blankj.utilcode.util.ResourceUtils
910
import com.osfans.trime.data.AppPrefs
10-
import com.osfans.trime.util.Const
11+
import com.osfans.trime.util.FileUtils
12+
import com.osfans.trime.util.ResourceUtils
1113
import com.osfans.trime.util.WeakHashSet
14+
import com.osfans.trime.util.appContext
15+
import kotlinx.serialization.json.Json
1216
import timber.log.Timber
1317
import java.io.File
18+
import java.util.concurrent.locks.ReentrantLock
19+
import kotlin.concurrent.withLock
1420

1521
object DataManager {
1622
private const val DEFAULT_CUSTOM_FILE_NAME = "default.custom.yaml"
23+
24+
private const val DATA_CHECKSUMS_NAME = "checksums.json"
25+
26+
private val lock = ReentrantLock()
27+
28+
private val json by lazy { Json }
29+
30+
private fun deserializeDataChecksums(raw: String): DataChecksums {
31+
return json.decodeFromString<DataChecksums>(raw)
32+
}
33+
34+
// If Android version supports direct boot, we put the hierarchy in device encrypted storage
35+
// instead of credential encrypted storage so that data can be accessed before user unlock
36+
private val dataDir: File =
37+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
38+
Timber.d("Using device protected storage")
39+
appContext.createDeviceProtectedStorageContext().dataDir
40+
} else {
41+
File(appContext.applicationInfo.dataDir)
42+
}
43+
44+
private fun AssetManager.dataChecksums(): DataChecksums {
45+
return open(DATA_CHECKSUMS_NAME)
46+
.bufferedReader()
47+
.use { it.readText() }
48+
.let { deserializeDataChecksums(it) }
49+
}
50+
1751
private val prefs get() = AppPrefs.defaultInstance()
1852

1953
val defaultDataDirectory = File(PathUtils.getExternalStoragePath(), "rime")
@@ -44,14 +78,6 @@ object DataManager {
4478
val userDataDir
4579
get() = File(prefs.profile.userDataDir)
4680

47-
sealed class Diff {
48-
object New : Diff()
49-
50-
object Update : Diff()
51-
52-
object Keep : Diff()
53-
}
54-
5581
/**
5682
* Return the absolute path of the compiled config file
5783
* based on given resource id.
@@ -72,48 +98,38 @@ object DataManager {
7298
return defaultPath.absolutePath
7399
}
74100

75-
private fun diff(
76-
old: String,
77-
new: String,
78-
): Diff {
79-
return when {
80-
old.isBlank() -> Diff.New
81-
!new.contentEquals(old) -> Diff.Update
82-
else -> Diff.Keep
83-
}
84-
}
85-
86-
@JvmStatic
87-
fun sync() {
88-
val newHash = Const.buildCommitHash
89-
val oldHash = prefs.internal.lastBuildGitHash
90-
91-
diff(oldHash, newHash).run {
92-
Timber.d("Diff: $this")
93-
when (this) {
94-
is Diff.New ->
95-
ResourceUtils.copyFileFromAssets(
96-
"rime",
97-
sharedDataDir.absolutePath,
98-
)
99-
is Diff.Update ->
100-
ResourceUtils.copyFileFromAssets(
101-
"rime",
102-
sharedDataDir.absolutePath,
103-
)
104-
is Diff.Keep -> {}
101+
fun sync() =
102+
lock.withLock {
103+
val oldChecksumsFile = File(dataDir, DATA_CHECKSUMS_NAME)
104+
val oldChecksums =
105+
oldChecksumsFile
106+
.runCatching { deserializeDataChecksums(bufferedReader().use { it.readText() }) }
107+
.getOrElse { DataChecksums("", emptyMap()) }
108+
109+
val newChecksums = appContext.assets.dataChecksums()
110+
111+
DataDiff.diff(oldChecksums, newChecksums).sortedByDescending { it.ordinal }.forEach {
112+
Timber.d("Diff: $it")
113+
when (it) {
114+
is DataDiff.CreateFile,
115+
is DataDiff.UpdateFile,
116+
-> ResourceUtils.copyFile(it.path, sharedDataDir)
117+
is DataDiff.DeleteDir,
118+
is DataDiff.DeleteFile,
119+
-> FileUtils.delete(sharedDataDir.resolve(it.path)).getOrThrow()
120+
}
105121
}
106-
}
107122

108-
// FIXME:缺失 default.custom.yaml 会导致方案列表为空
109-
with(File(sharedDataDir, DEFAULT_CUSTOM_FILE_NAME)) {
110-
val customDefault = this
111-
if (!customDefault.exists()) {
112-
Timber.d("Creating empty default.custom.yaml ...")
113-
customDefault.createNewFile()
123+
ResourceUtils.copyFile(DATA_CHECKSUMS_NAME, dataDir)
124+
125+
// FIXME:缺失 default.custom.yaml 会导致方案列表为空
126+
File(sharedDataDir, DEFAULT_CUSTOM_FILE_NAME).let {
127+
if (!it.exists()) {
128+
Timber.d("Creating empty default.custom.yaml")
129+
it.bufferedWriter().use { w -> w.write("") }
130+
}
114131
}
115-
}
116132

117-
Timber.i("Synced!")
118-
}
133+
Timber.d("Synced!")
134+
}
119135
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
package com.osfans.trime.util
6+
7+
import java.io.File
8+
import java.io.IOException
9+
10+
object FileUtils {
11+
fun delete(file: File) =
12+
runCatching {
13+
if (!file.exists()) return Result.success(Unit)
14+
val res =
15+
if (file.isDirectory) {
16+
file.walkBottomUp()
17+
.fold(true) { acc, file ->
18+
if (file.exists()) file.delete() else acc
19+
}
20+
} else {
21+
file.delete()
22+
}
23+
if (!res) {
24+
throw IOException("Cannot delete ${file.path}")
25+
}
26+
}
27+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
package com.osfans.trime.util
6+
7+
import java.io.File
8+
9+
object ResourceUtils {
10+
fun copyFile(
11+
filename: String,
12+
dest: File,
13+
) = runCatching {
14+
appContext.assets.open(filename).use { i ->
15+
File(dest, filename)
16+
.also { it.parentFile?.mkdirs() }
17+
.outputStream()
18+
.use { o -> i.copyTo(o) }
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)