Skip to content

Commit

Permalink
feat: smarter and faster assets syncing
Browse files Browse the repository at this point in the history
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
  • Loading branch information
WhiredPlanck committed May 3, 2024
1 parent ff6b838 commit 571e7c4
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 62 deletions.
12 changes: 0 additions & 12 deletions app/src/main/java/com/osfans/trime/data/AppPrefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,12 @@ class AppPrefs(

class Internal(private val prefs: AppPrefs) {
companion object {
const val LAST_VERSION_NAME = "general__last_version_name"
const val PID = "general__pid"
const val LAST_BUILD_GIT_HASH = "general__last_build_git_hash"
}

var lastVersionName: String
get() = prefs.getPref(LAST_VERSION_NAME, "")
set(v) = prefs.setPref(LAST_VERSION_NAME, v)
var pid: Int
get() = prefs.getPref(PID, 0)
set(v) = prefs.setPref(PID, v)
var lastBuildGitHash: String
get() = prefs.getPref(LAST_BUILD_GIT_HASH, "")
set(v) = prefs.setPref(LAST_BUILD_GIT_HASH, v)
}

/**
Expand Down Expand Up @@ -420,7 +412,6 @@ class AppPrefs(
const val UI_MODE = "other__ui_mode"
const val SHOW_APP_ICON = "other__show_app_icon"
const val SHOW_STATUS_BAR_ICON = "other__show_status_bar_icon"
const val DESTROY_ON_QUIT = "other__destroy_on_quit"
}

var uiMode: String
Expand All @@ -432,8 +423,5 @@ class AppPrefs(
var showStatusBarIcon: Boolean = false
get() = prefs.getPref(SHOW_STATUS_BAR_ICON, false)
private set
var destroyOnQuit: Boolean = false
get() = prefs.getPref(DESTROY_ON_QUIT, false)
private set
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/com/osfans/trime/data/base/DataChecksums.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later

package com.osfans.trime.data.base

import kotlinx.serialization.Serializable

@Serializable
data class DataChecksums(
val sha256: String,
val files: Map<String, String>,
)
55 changes: 55 additions & 0 deletions app/src/main/java/com/osfans/trime/data/base/DataDiff.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later

package com.osfans.trime.data.base

sealed interface DataDiff {
val path: String

val ordinal: Int

data class CreateFile(override val path: String) : DataDiff {
override val ordinal: Int
get() = 3
}

data class UpdateFile(override val path: String) : DataDiff {
override val ordinal: Int
get() = 2
}

data class DeleteDir(override val path: String) : DataDiff {
override val ordinal: Int
get() = 1
}

data class DeleteFile(override val path: String) : DataDiff {
override val ordinal: Int
get() = 0
}

companion object {
fun diff(
old: DataChecksums,
new: DataChecksums,
): List<DataDiff> {
if (old.sha256 == new.sha256) return emptyList()
return new.files.mapNotNull { (path, sha256) ->
when {
path !in old.files && sha256.isNotBlank() -> CreateFile(path)
old.files[path] != sha256 ->
if (sha256.isNotBlank()) UpdateFile(path) else null
else -> null
}
}.toMutableList().apply {
addAll(
old.files.filterKeys { it !in new.files }
.map { (path, sha256) ->
if (sha256.isNotBlank()) DeleteFile(path) else DeleteDir(path)
},
)
}
}
}
}
116 changes: 66 additions & 50 deletions app/src/main/java/com/osfans/trime/data/base/DataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,50 @@

package com.osfans.trime.data.base

import android.content.res.AssetManager
import android.os.Build
import com.blankj.utilcode.util.PathUtils
import com.blankj.utilcode.util.ResourceUtils
import com.osfans.trime.data.AppPrefs
import com.osfans.trime.util.Const
import com.osfans.trime.util.FileUtils
import com.osfans.trime.util.ResourceUtils
import com.osfans.trime.util.WeakHashSet
import com.osfans.trime.util.appContext
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.io.File
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

object DataManager {
private const val DEFAULT_CUSTOM_FILE_NAME = "default.custom.yaml"

private const val DATA_CHECKSUMS_NAME = "checksums.json"

private val lock = ReentrantLock()

private val json by lazy { Json }

private fun deserializeDataChecksums(raw: String): DataChecksums {
return json.decodeFromString<DataChecksums>(raw)
}

// If Android version supports direct boot, we put the hierarchy in device encrypted storage
// instead of credential encrypted storage so that data can be accessed before user unlock
private val dataDir: File =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Timber.d("Using device protected storage")
appContext.createDeviceProtectedStorageContext().dataDir
} else {
File(appContext.applicationInfo.dataDir)
}

private fun AssetManager.dataChecksums(): DataChecksums {
return open(DATA_CHECKSUMS_NAME)
.bufferedReader()
.use { it.readText() }
.let { deserializeDataChecksums(it) }
}

private val prefs get() = AppPrefs.defaultInstance()

val defaultDataDirectory = File(PathUtils.getExternalStoragePath(), "rime")
Expand Down Expand Up @@ -44,14 +78,6 @@ object DataManager {
val userDataDir
get() = File(prefs.profile.userDataDir)

sealed class Diff {
object New : Diff()

object Update : Diff()

object Keep : Diff()
}

/**
* Return the absolute path of the compiled config file
* based on given resource id.
Expand All @@ -72,48 +98,38 @@ object DataManager {
return defaultPath.absolutePath
}

private fun diff(
old: String,
new: String,
): Diff {
return when {
old.isBlank() -> Diff.New
!new.contentEquals(old) -> Diff.Update
else -> Diff.Keep
}
}

@JvmStatic
fun sync() {
val newHash = Const.buildCommitHash
val oldHash = prefs.internal.lastBuildGitHash

diff(oldHash, newHash).run {
Timber.d("Diff: $this")
when (this) {
is Diff.New ->
ResourceUtils.copyFileFromAssets(
"rime",
sharedDataDir.absolutePath,
)
is Diff.Update ->
ResourceUtils.copyFileFromAssets(
"rime",
sharedDataDir.absolutePath,
)
is Diff.Keep -> {}
fun sync() =
lock.withLock {
val oldChecksumsFile = File(dataDir, DATA_CHECKSUMS_NAME)
val oldChecksums =
oldChecksumsFile
.runCatching { deserializeDataChecksums(bufferedReader().use { it.readText() }) }
.getOrElse { DataChecksums("", emptyMap()) }

val newChecksums = appContext.assets.dataChecksums()

DataDiff.diff(oldChecksums, newChecksums).sortedByDescending { it.ordinal }.forEach {
Timber.d("Diff: $it")
when (it) {
is DataDiff.CreateFile,
is DataDiff.UpdateFile,
-> ResourceUtils.copyFile(it.path, sharedDataDir)
is DataDiff.DeleteDir,
is DataDiff.DeleteFile,
-> FileUtils.delete(sharedDataDir.resolve(it.path)).getOrThrow()
}
}
}

// FIXME:缺失 default.custom.yaml 会导致方案列表为空
with(File(sharedDataDir, DEFAULT_CUSTOM_FILE_NAME)) {
val customDefault = this
if (!customDefault.exists()) {
Timber.d("Creating empty default.custom.yaml ...")
customDefault.createNewFile()
ResourceUtils.copyFile(DATA_CHECKSUMS_NAME, dataDir)

// FIXME:缺失 default.custom.yaml 会导致方案列表为空
File(sharedDataDir, DEFAULT_CUSTOM_FILE_NAME).let {
if (!it.exists()) {
Timber.d("Creating empty default.custom.yaml")
it.bufferedWriter().use { w -> w.write("") }
}
}
}

Timber.i("Synced!")
}
Timber.d("Synced!")
}
}
27 changes: 27 additions & 0 deletions app/src/main/java/com/osfans/trime/util/FileUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later

package com.osfans.trime.util

import java.io.File
import java.io.IOException

object FileUtils {
fun delete(file: File) =
runCatching {
if (!file.exists()) return Result.success(Unit)
val res =
if (file.isDirectory) {
file.walkBottomUp()
.fold(true) { acc, file ->
if (file.exists()) file.delete() else acc
}
} else {
file.delete()
}
if (!res) {
throw IOException("Cannot delete ${file.path}")
}
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/com/osfans/trime/util/ResourceUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later

package com.osfans.trime.util

import java.io.File

object ResourceUtils {
fun copyFile(
filename: String,
dest: File,
) = runCatching {
appContext.assets.open(filename).use { i ->
File(dest, filename)
.also { it.parentFile?.mkdirs() }
.outputStream()
.use { o -> i.copyTo(o) }
}
}
}

0 comments on commit 571e7c4

Please sign in to comment.