diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0fc26c28f5..e46dc95b8f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,20 +1,11 @@ @file:Suppress("UnstableApiUsage") -import android.databinding.tool.ext.capitalizeUS import com.android.build.gradle.internal.tasks.factory.dependsOn -import com.google.common.hash.Hashing -import com.google.common.io.Files -import groovy.json.JsonOutput -import groovy.json.JsonSlurper -import java.io.ByteArrayOutputStream -import java.nio.charset.Charset -import java.io.StringWriter import java.util.Properties -import java.util.TimeZone -import java.util.Date -import java.text.SimpleDateFormat +import org.gradle.configurationcache.extensions.capitalized plugins { + id("com.osfans.trime.data-checksums") id("com.android.application") kotlin("android") kotlin("plugin.serialization") version Versions.kotlin @@ -22,42 +13,6 @@ plugins { id("com.mikepenz.aboutlibraries.plugin") } -fun exec(cmd: String): String = ByteArrayOutputStream().use { - project.exec { - commandLine = cmd.split(" ") - standardOutput = it - } - it.toString().trim() -} -fun envOrDefault(env: String, default: () -> String): String { - val v = System.getenv(env) - return if (v.isNullOrBlank()) default() else v -} - -val gitUserOrCIName = envOrDefault("CI_NAME") { - exec("git config user.name") -} -val gitVersionName = exec("git describe --tags --long --always") -val gitHashShort = exec("git rev-parse --short HEAD") -val gitRemoteUrl = exec("git remote get-url origin") - .replaceFirst("^git@github\\.com:", "https://github.com/") - .replaceFirst("\\.git\$", "") - -fun buildInfo(): String { - val writer = StringWriter() - val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").apply { - timeZone = TimeZone.getTimeZone("UTC") - }.format(Date(System.currentTimeMillis())) - writer.append("Builder: ${gitUserOrCIName}\\n") - writer.append("Build Time: $time UTC\\n") - writer.append("Build Version Name: ${gitVersionName}\\n") - writer.append("Git Hash: ${gitHashShort}\\n") - writer.append("Git Repo: $gitRemoteUrl") - val info = writer.toString() - println(info) - return info -} - android { namespace = "com.osfans.trime" compileSdk = 34 @@ -73,10 +28,11 @@ android { multiDexEnabled = true setProperty("archivesBaseName", "trime-$versionName") - buildConfigField("String", "BUILD_GIT_HASH", "\"${gitHashShort}\"") - buildConfigField("String", "BUILD_GIT_REPO", "\"${gitRemoteUrl}\"") - buildConfigField("String", "BUILD_VERSION_NAME", "\"${gitVersionName}\"") - buildConfigField("String", "BUILD_INFO", "\"${buildInfo()}\"") + buildConfigField("String", "BUILDER", "\"${project.builder}\"") + buildConfigField("long", "BUILD_TIMESTAMP", project.buildTimestamp) + buildConfigField("String", "BUILD_COMMIT_HASH", "\"${project.buildCommitHash}\"") + buildConfigField("String", "BUILD_GIT_REPO", "\"${project.buildGitRepo}\"") + buildConfigField("String", "BUILD_VERSION_NAME", "\"${project.buildVersionName}\"") } signingConfigs { @@ -184,20 +140,13 @@ ksp { arg("room.schemaLocation", "$projectDir/schemas") } -val generateDataChecksum by tasks.register("generateDataChecksum") { - inputDir.set(file("src/main/assets")) - outputFile.set(file("src/main/assets/checksums.json")) -} - android.applicationVariants.all { - val variantName = name.capitalizeUS() - tasks.findByName("merge${variantName}Assets")?.dependsOn(generateDataChecksum) + val variantName = name.capitalized() + tasks.findByName("generateDataChecksums")?.also { + tasks.getByName("merge${variantName}Assets").dependsOn(it) + } } -tasks.register("cleanGeneratedAssets") { - delete(file("src/main/assets/checksums.json")) -}.also { tasks.clean.dependsOn(it) } - tasks.register("cleanCxxIntermediates") { delete(file(".cxx")) }.also { tasks.clean.dependsOn(it) } @@ -236,83 +185,3 @@ dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("junit:junit:4.13.2") } - -abstract class DataChecksumsTask : DefaultTask() { - @get:Incremental - @get:PathSensitive(PathSensitivity.NAME_ONLY) - @get:InputDirectory - abstract val inputDir: DirectoryProperty - - @get:OutputFile - abstract val outputFile: RegularFileProperty - - private val file by lazy { outputFile.get().asFile } - - private fun serialize(map: Map) { - file.deleteOnExit() - file.writeText( - JsonOutput.prettyPrint( - JsonOutput.toJson( - mapOf( - "sha256" to Hashing.sha256() - .hashString( - map.entries.joinToString { it.key + it.value }, - Charset.defaultCharset() - ) - .toString(), - "files" to map - ) - ) - ) - ) - } - - @Suppress("UNCHECKED_CAST") - private fun deserialize(): Map = - ((JsonSlurper().parseText(file.readText()) as Map))["files"] as Map - - companion object { - fun sha256(file: File): String = - Files.asByteSource(file).hash(Hashing.sha256()).toString() - } - - @TaskAction - fun execute(inputChanges: InputChanges) { - val map = - file.exists() - .takeIf { it } - ?.runCatching { - deserialize() - // remove all old dirs - .filterValues { it.isNotBlank() } - .toMutableMap() - } - ?.getOrNull() - ?: mutableMapOf() - - fun File.allParents(): List = - if (parentFile == null || parentFile.path in map) - listOf() - else - listOf(parentFile) + parentFile.allParents() - inputChanges.getFileChanges(inputDir).forEach { change -> - if (change.file.name == file.name) - return@forEach - logger.log(LogLevel.DEBUG, "${change.changeType}: ${change.normalizedPath}") - val relativeFile = change.file.relativeTo(file.parentFile) - val key = relativeFile.path - if (change.changeType == ChangeType.REMOVED) { - map.remove(key) - } else { - map[key] = sha256(change.file) - } - } - // calculate dirs - inputDir.asFileTree.forEach { - it.relativeTo(file.parentFile).allParents().forEach { p -> - map[p.path] = "" - } - } - serialize(map.toSortedMap()) - } -} diff --git a/app/src/main/java/com/osfans/trime/ui/fragments/AboutFragment.kt b/app/src/main/java/com/osfans/trime/ui/fragments/AboutFragment.kt index 5680d145d1..d4f9985149 100644 --- a/app/src/main/java/com/osfans/trime/ui/fragments/AboutFragment.kt +++ b/app/src/main/java/com/osfans/trime/ui/fragments/AboutFragment.kt @@ -9,13 +9,13 @@ import androidx.navigation.fragment.findNavController import androidx.preference.Preference import androidx.preference.get import com.blankj.utilcode.util.ToastUtils -import com.osfans.trime.BuildConfig import com.osfans.trime.R import com.osfans.trime.core.Rime import com.osfans.trime.data.opencc.OpenCCDictManager import com.osfans.trime.ui.components.PaddingPreferenceFragment import com.osfans.trime.ui.main.MainViewModel import com.osfans.trime.util.Const +import com.osfans.trime.util.formatDateTime import com.osfans.trime.util.optionalPreference import com.osfans.trime.util.thirdPartySummary import splitties.systemservices.clipboardManager @@ -35,11 +35,18 @@ class AboutFragment : PaddingPreferenceFragment() { intent = Intent( Intent.ACTION_VIEW, - Uri.parse("${Const.currentGitRepo}/commits/${Const.buildGitHash}"), + Uri.parse("${Const.currentGitRepo}/commits/${Const.buildCommitHash}"), ) } - get("about__buildinfo")?.apply { - summary = BuildConfig.BUILD_INFO + get("about__build_info")?.apply { + summary = + requireContext().getString( + R.string.about__build_info_format, + Const.builder, + formatDateTime(Const.buildTimestamp), + Const.buildCommitHash, + Const.currentGitRepo, + ) setOnPreferenceClickListener { val info = ClipData.newPlainText("BuildInfo", summary) clipboardManager.setPrimaryClip(info) diff --git a/app/src/main/java/com/osfans/trime/util/Const.kt b/app/src/main/java/com/osfans/trime/util/Const.kt index c247bb09fe..d346847a77 100644 --- a/app/src/main/java/com/osfans/trime/util/Const.kt +++ b/app/src/main/java/com/osfans/trime/util/Const.kt @@ -3,7 +3,9 @@ package com.osfans.trime.util import com.osfans.trime.BuildConfig object Const { - val buildGitHash = BuildConfig.BUILD_GIT_HASH + val builder = BuildConfig.BUILDER + val buildTimestamp = BuildConfig.BUILD_TIMESTAMP + val buildCommitHash = BuildConfig.BUILD_COMMIT_HASH val displayVersionName = "${BuildConfig.BUILD_VERSION_NAME}-${BuildConfig.BUILD_TYPE}" val originalGitRepo = "https://github.com/osfans/trime" val currentGitRepo = BuildConfig.BUILD_GIT_REPO diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index cb1c7417ba..f3596cf0a1 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -40,6 +40,7 @@ 贡献代码 微软平台PRIME输入法 隐私策略 + 构建者:%1$s\nGit 仓库: %2$s\n构建 Git 哈希: %3$s\n构建时间: %4$s 启用 启用同文输入法 选取软键盘 @@ -100,7 +101,7 @@ 后台同步 点击以开启后台同步 定时同步 - 已开启定时同步 下次同步时间: %s + 已开启定时同步 下次同步时间: %1$s 点击以开启定时同步 应用市场 用户社区 @@ -181,7 +182,7 @@ 允许触发按键的滑动手势 触发按键滑动手势的速度(距离/速度满足其一即可) 连续击键时触发滑动手势的速度 - 编译信息 + 构建信息 连接实体键盘时,显示迷你软键盘 修改版QQ群 候选栏 @@ -234,7 +235,7 @@ 维护 正在加载 恢复默认设置 - 上次后台同步时间:%s\n状态:%s + 上次后台同步时间:%1$s\n状态:%2$s 成功 失败 设定存储位置和修改同步设置等 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e323deeac0..7d88a0ac3e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -41,6 +41,7 @@ 貢獻代碼 Win10 PRIME輸入法平臺 隱私權政策 + 建構者:%1$s\nGit 倉庫: %2$s\n建構 Git 雜湊值: %3$s\n建構時間: %4$s 啓用 啓用同文輸入法平臺 選取軟鍵盤 @@ -105,7 +106,7 @@ 從未在後臺同步過 點擊以啟用後台同步 定時同步 - 已啟用定時同步 下次同步時間: %s + 已啟用定時同步 下次同步時間: %1$s 點擊以啟用定時同步 應用市場 使用者社群 @@ -185,7 +186,7 @@ 允許觸發按鍵的滑動手勢 觸發按鍵滑動手勢的速度(距離/速度滿足其一即可) 連續擊鍵時觸發滑動手勢的速度 - 編譯信息 + 建構資訊 連接實體鍵盤時,顯示迷你軟鍵盤 修改版QQ羣 候選欄 @@ -238,7 +239,7 @@ 維護 正在載入 後臺同步 - 上次後臺同步時間:%s\n狀態:%s + 上次後臺同步時間:%1$s\n狀態:%2$s 成功 失敗 剪貼板內容 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d11d9ce520..c616d0e40d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,6 +41,7 @@ Contribute Win10 PRIME Platform Privacy Policy + Builder: %1$s\nGit Repo: %2$s\nBuild Git Hash: %3$s\nBuild Time: %4$s Enable Enable Trime Change Keyboard @@ -103,11 +104,11 @@ Shared directory User directory Sync in background - Last sync in background: %s\nStatus: %s + Last sync in background: %1$s\nStatus: %2$s Never synced in the background Click to enable Timing sync - Timing sync is enabled Next sync at time: %s + Timing sync is enabled Next sync at time: %1$s Click to enable Marketplace User Community @@ -189,7 +190,7 @@ Allow swipe gestures to trigger keys The speed of triggering the button swipe gesture (the distance/velocity is sufficient) The velocity of the swipe gesture on consecutive keystrokes - Build info + Build Info Show mini keyboard with real keyboard attached Custom QQ Group diff --git a/app/src/main/res/xml/about_preference.xml b/app/src/main/res/xml/about_preference.xml index 20d92671e1..d1fb9b633a 100644 --- a/app/src/main/res/xml/about_preference.xml +++ b/app/src/main/res/xml/about_preference.xml @@ -11,8 +11,8 @@ app:iconSpaceReserved="false"> - /> diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 876c922b22..15980a145a 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,7 +1,25 @@ plugins { `kotlin-dsl` + kotlin("plugin.serialization") version embeddedKotlinVersion } repositories { + google() mavenCentral() + gradlePluginPortal() +} + +dependencies { + compileOnly("com.android.tools.build:gradle:8.2.0") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.1") +} + +gradlePlugin { + plugins { + register("dataChecksums") { + id = "com.osfans.trime.data-checksums" + implementationClass = "DataChecksumsPlugin" + } + } } diff --git a/buildSrc/src/main/kotlin/DataChecksumsPlugin.kt b/buildSrc/src/main/kotlin/DataChecksumsPlugin.kt new file mode 100644 index 0000000000..7f9269c9e1 --- /dev/null +++ b/buildSrc/src/main/kotlin/DataChecksumsPlugin.kt @@ -0,0 +1,125 @@ +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Delete +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.task +import org.gradle.work.ChangeType +import org.gradle.work.Incremental +import org.gradle.work.InputChanges +import org.jetbrains.kotlin.com.google.common.hash.Hashing +import org.jetbrains.kotlin.com.google.common.io.ByteSource +import java.io.File +import java.nio.charset.Charset +import kotlin.collections.set + +/** + * Add task generateDataChecksums + */ +class DataChecksumsPlugin : Plugin { + companion object { + const val TASK = "generateDataChecksums" + const val CLEAN_TASK = "cleanDatacheksums" + const val FILE_NAME = "checksums.json" + } + + override fun apply(target: Project) { + target.task(TASK) { + inputDir.set(target.assetsDir) + outputFile.set(target.assetsDir.resolve(FILE_NAME)) + } + target.task(CLEAN_TASK) { + delete(target.assetsDir.resolve(FILE_NAME)) + }.also { + target.tasks.findByName("clean")?.dependsOn(it) + } + } + + abstract class DataChecksumsTask : DefaultTask() { + @Serializable + data class DataChecksums( + val sha256: String, + val files: Map, + ) + + @get:Incremental + @get:PathSensitive(PathSensitivity.NAME_ONLY) + @get:InputDirectory + abstract val inputDir: DirectoryProperty + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + private val file by lazy { outputFile.get().asFile } + + private fun serialize(files: Map) { + val checksums = + DataChecksums( + Hashing.sha256() + .hashString( + files.entries.joinToString { it.key + it.value }, + Charset.defaultCharset(), + ).toString(), + files, + ) + file.writeText(json.encodeToString(checksums)) + } + + private fun deserialize(): Map = json.decodeFromString(file.readText()).files + + companion object { + fun sha256(file: File): String = ByteSource.wrap(file.readBytes()).hash(Hashing.sha256()).toString() + } + + @TaskAction + fun execute(inputChanges: InputChanges) { + val map = + file.exists() + .takeIf { it } + ?.runCatching { + deserialize() + // remove all old dirs + .filterValues { it.isNotBlank() } + .toMutableMap() + } + ?.getOrNull() + ?: mutableMapOf() + + fun File.allParents(): List = + if (parentFile == null || parentFile.path in map) { + listOf() + } else { + listOf(parentFile) + parentFile.allParents() + } + inputChanges.getFileChanges(inputDir).forEach { change -> + if (change.file.name == file.name) { + return@forEach + } + logger.log(LogLevel.DEBUG, "${change.changeType}: ${change.normalizedPath}") + val relativeFile = change.file.relativeTo(file.parentFile) + val key = relativeFile.path + if (change.changeType == ChangeType.REMOVED) { + map.remove(key) + } else { + map[key] = sha256(change.file) + } + } + // calculate dirs + inputDir.asFileTree.forEach { + it.relativeTo(file.parentFile).allParents().forEach { p -> + map[p.path] = "" + } + } + serialize(map.toSortedMap()) + } + } +} diff --git a/buildSrc/src/main/kotlin/Utils.kt b/buildSrc/src/main/kotlin/Utils.kt new file mode 100644 index 0000000000..5c51a7abb2 --- /dev/null +++ b/buildSrc/src/main/kotlin/Utils.kt @@ -0,0 +1,76 @@ +import kotlinx.serialization.json.Json +import org.gradle.api.Project +import org.gradle.api.Task +import java.io.ByteArrayOutputStream +import java.io.File + +inline fun envOrDefault( + env: String, + default: () -> String, +) = System.getenv(env)?.takeIf { it.isNotBlank() } ?: default() + +inline fun Project.propertyOrDefault( + prop: String, + default: () -> String, +) = runCatching { property(prop)!!.toString() }.getOrElse { + default() +} + +fun Project.runCmd(cmd: String): String = + ByteArrayOutputStream().use { + project.exec { + commandLine = cmd.split(" ") + standardOutput = it + } + it.toString().trim() + } + +val json = Json { prettyPrint = true } + +internal inline fun Project.envOrProp( + env: String, + prop: String, + block: () -> String, +) = envOrDefault(env) { + propertyOrDefault(prop) { + block() + } +} + +val Project.assetsDir: File + get() = file("src/main/assets").also { it.mkdirs() } + +val Project.cleanTask: Task + get() = tasks.getByName("clean") + +val Project.builder + get() = + envOrProp("CI_NAME", "ciName") { + runCmd("git config user.name") + } + +val Project.buildGitRepo + get() = + envOrProp("BUILD_GIT_REPO", "buildGitRepo") { + runCmd("git remote get-url origin") + .replaceFirst("^git@github\\.com:", "https://github.com/") + .replaceFirst("\\.git\$", "") + } + +val Project.buildVersionName + get() = + envOrProp("BUILD_VERSION_NAME", "buildVersionName") { + runCmd("git describe --tags --long --always") + } + +val Project.buildCommitHash + get() = + envOrProp("BUILD_COMMIT_HASH", "buildCommitHash") { + runCmd("git rev-parse HEAD") + } + +val Project.buildTimestamp + get() = + envOrProp("BUILD_TIMESTAMP", "buildTimestamp") { + System.currentTimeMillis().toString() + }