同步移动端工程、设备控制改进与发布资料

This commit is contained in:
2026-09-26 19:21:12 +08:00
parent 912ba432cf
commit 98eadc5c8b
137 changed files with 9274 additions and 85 deletions
@@ -0,0 +1,62 @@
# Android 环境、构建与交付
## 安装门槛
Android Studio 是正常安装软件,由用户运行已核验官方安装包。安装包放用户普通下载目录,不放仓库或 Codex bin。准备好后停止等待用户安装,不自动运行安装器。安装时建议采用默认位置,首次向导安装 Android SDK;不必创建项目,也不必先下载模拟器或 NDK。
首次完成后记录实际安装版本、JDK、SDK、Build Tools、Kotlin、Compose、AGP 与 Gradle 的兼容组合,锁入版本目录、Wrapper 和依赖校验文件。安装位置用环境探测/local.properties,不写到可提交源码。官方参考:
- https://developer.android.com/studio
- https://developer.android.com/studio/install
- https://developer.android.com/kotlin/multiplatform/plugin
- https://kotlinlang.org/docs/multiplatform/multiplatform-compatibility-guide.html
## 工程与路径
源码位于相邻 `安卓程序源代码/`,模块 androidApp、sharedCore、sharedUi。在 Android Studio 打开工程根,不分别打开子模块。先验证中文和空格路径;若 Android 工具拒绝该路径,通过受保护的仓库外 ASCII 构建目录同步源码并执行,结果回收到交付目录。同步只包含源码及明确资源,不复制私有登记、密钥、缓存或 `.git`;脚本记录源摘要,不覆盖用户目录。
已安装工具链:Android Studio Quail 4 Patch 1(用户确认中文)、JBR 25.0.3、SDK 37;工程固定 Gradle 9.5.0、AGP 9.3.1、Kotlin 2.4.20、Compose 1.12.1。Wrapper 使用官方发行地址和 SHA-256。首次 `:androidApp:assembleDebug` 与 `:sharedCore:jvmTest` 已在隔离 ASCII 路径通过;这只是骨架及协议验证,不等于功能验收。lint 与真实 BLE 已有通过记录,具体覆盖范围见测试结果归档。
在仓库根执行下列命令,参数替换为本机实际目录,不把替换后的路径提交:
```text
python "移动端相关内容/安卓app/构建与发布/build_android.py" --work-root "<受保护临时目录的ASCII路径>" --jdk "<Android Studio的jbr目录>" --sdk "<Android SDK目录>"
```
脚本每次创建独立 `qms-build-*` 子目录,只复制工程源码,忽略 build、.gradle、.kotlin、local.properties。默认使用 Wrapper;可用 `--gradle` 指定已核验的同版本 Gradle,避免重复下载。默认运行 JVM 测试和 assembleDebug,可在末尾显式传任务列表。保留构建目录用于查看测试报告和 `androidApp/build/outputs/apk/debug/androidApp-debug.apk`;验证后只清理本次明确目录。Android 调试签名沿用用户默认 Android 密钥目录,不复制到源码或隔离工程。
路径实测:AGP 默认拒绝非 ASCII 路径;启用 `android.overridePathCheck=true` 后编译继续,但当前 JBR/Gradle 下 JVM 测试出现类加载失败。在 ASCII 隔离目录同一测试通过,故当前使用该回退。2026-09-25 已在全新且包含空格的 ASCII 构建根目录重新执行全部 92 个任务,JVM 测试及两个 debug APK 均成功;保留中文源码位置。
## APK 与签名
- 应用名“奇妙小屏幕”,applicationId 为 `org.qimiaoscreen.controller`,minSdk 26,首版版本 0.1.0。
- 仅 debug 签名,沿用本机调试密钥并记录证书摘要(不记录私钥)。正式签名只有用户明确命令才配置或执行。
- 调试签名更换可能无法覆盖安装;不能自动卸载并清除用户配置来掩盖。需要时先报告原因。
- App versionCode 独立递增,设备版本/协议版本分别登记,不能因设备部署自动提升 App 版本。
- 每次交付放 `交付包/<App版本>-<versionCode>/`:APK、SHA256SUMS、manifest.json、README.md。manifest 包含 App/协议/兼容设备信息、源摘要、工具链版本和签名证书摘要,不包含本机路径或测试机标识。
- APK 与发布元数据保留在私有 Git 仓库,需要时沿用 Git LFS;构建缓存、SDK、真实登记、调试及正式签名私钥都不提交。
- 本任务不导出核桃派 IMG/OTA;设备功能时间按根规则更新,设备发布版本不因普通部署占号。
## 交付前检查
从隔离目录依文档重新构建并验证;完成相关测试、协议互操作和真实 BLE 联调;运行根仓库卫生检查。交付说明准确区分通过、未验证和不适用。第二台手机验收 APK 放到同一清晰可找到的交付目录,用户安装后再进行实机互斥测试。
## 可重复打包
```text
python "移动端相关内容/安卓app/构建与发布/package_debug.py" --build "<本轮隔离工程目录>" --sdk "<Android SDK目录>"
```
运行前将 JAVA_HOME 指向正常安装的 Android Studio jbr。脚本逐文件核对隔离工程与当前源码的 SHA-256,调用 Build Tools 36.0.0 的 apksigner 验证 Android Debug 证书,创建版本独立目录,保存 APK、摘要、工具链和源文件清单。已存在交付目录拒绝覆盖。结果默认标为开发验证包,必须结合测试报告说明验收范围,不用打包成功替代验收。versionCode 2 包含本轮连接清理和界面信息补充,可覆盖同证书的 code 1。
本轮 App 0.2.0 / versionCode 3 为四栏界面及设备记忆优化,仍仅 debug 签名。关于页显示本次 Gradle 构建日期(北京时间),不修改设备发布版本。
## 构建排障与测试复盘
手动调用隔离工程 Wrapper 时,每个新 shell 都须明确使用已登记的 JDK/SDK(JAVA_HOME/ANDROID_HOME);前一工具调用的进程环境不会自动传到下一次调用。缺少 SDK location 时先检查环境,不重复下载工具、不改动中文源码位置。
Gradle Kotlin DSL 里 `java` 可能被插件扩展名遮蔽;构建日期实现显式 import `ZonedDateTime`、`ZoneId`、`DateTimeFormatter`,避免直接以 `java.time...` 引用引发解析错误。日期仍使用北京时间,不通过硬编码本机路径解决。
Python 在 Windows 缺少 IANA tzdata 时,固定北京时间时间戳可使用标准库 `timezone(timedelta(hours=8))`;这一方法只适用于明确要求的固定 UTC+08:00 时间,不当作任意地区夏令时转换方案,也不因该情况自动新增依赖。
打包前重新核对隔离源码、实际 APK、构建日期和签名摘要;测试源码调整也要同步隔离工程,不能测试旧测试 APK。测试/构建遇到的新问题在完成后更新相应文档并在最终反馈说明,具体输入和真机排障见 [测试排障指南](../如何安卓测试/常见问题与排障.md)。
@@ -0,0 +1,40 @@
"""Build from an isolated path; source stays in its original Chinese directory."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--work-root', type=Path, required=True)
parser.add_argument('--jdk', type=Path, required=True)
parser.add_argument('--sdk', type=Path, required=True)
parser.add_argument('--gradle', type=Path, help='Optional verified Gradle executable; otherwise use Wrapper')
parser.add_argument('tasks', nargs='*', default=[':sharedCore:jvmTest', ':androidApp:assembleDebug'])
args = parser.parse_args()
root = args.work_root.resolve()
if not str(root).isascii():
parser.error('work-root must use ASCII characters')
if not (args.jdk / 'bin/java.exe').is_file() or not (args.sdk / 'platforms').is_dir():
parser.error('JDK or SDK is missing')
root.mkdir(parents=True, exist_ok=True)
destination = Path(tempfile.mkdtemp(prefix='qms-build-', dir=root))
source = Path(__file__).resolve().parents[1] / '安卓程序源代码'
shutil.copytree(source, destination, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('build', '.gradle', '.kotlin', 'local.properties'))
environment = os.environ.copy()
environment.update(JAVA_HOME=str(args.jdk.resolve()), ANDROID_HOME=str(args.sdk.resolve()))
executable = args.gradle.resolve() if args.gradle else destination / 'gradlew.bat'
result = subprocess.run([str(executable), '--console', 'plain', *args.tasks], cwd=destination, env=environment)
# Keep this exact directory for logs, reports and APK retrieval, even on failure.
print(f'Build directory: {destination}', flush=True)
return result.returncode
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,65 @@
"""Package an independently built debug APK without overwriting an old delivery."""
import argparse
import hashlib
import json
from pathlib import Path
import re
import shutil
import subprocess
import tomllib
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--build', type=Path, required=True)
parser.add_argument('--sdk', type=Path, required=True)
args = parser.parse_args()
root = Path(__file__).resolve().parent
source = root.parent / '安卓程序源代码'
config = (source / 'androidApp/build.gradle.kts').read_text(encoding='utf-8')
version = re.search(r'versionName = "([^"]+)"', config)[1]
code = int(re.search(r'versionCode = (\d+)', config)[1])
entries = []
for path in sorted(source.rglob('*')):
relative = path.relative_to(source)
if not path.is_file() or any(p in {'build', '.gradle', '.kotlin'} for p in relative.parts) or path.name == 'local.properties':
continue
built = args.build / relative
if not built.is_file() or digest(built) != digest(path):
raise RuntimeError('Build source differs from current source: ' + relative.as_posix())
entries.append(dict(path=relative.as_posix(), sha256=digest(path)))
apk = args.build / 'androidApp/build/outputs/apk/debug/androidApp-debug.apk'
signer = args.sdk / 'build-tools/36.0.0/apksigner.bat'
result = subprocess.run([str(signer), 'verify', '--print-certs', str(apk)],
capture_output=True, text=True, encoding='utf-8', errors='replace')
if result.returncode or 'CN=Android Debug' not in result.stdout:
raise RuntimeError('APK must verify with the existing Android Debug certificate')
certificate = re.search(r'certificate SHA-256 digest: ([0-9a-fA-F]+)', result.stdout)[1].lower()
destination = root / '交付包' / f'{version}-{code}'
destination.mkdir(parents=True, exist_ok=False)
name = f'奇妙小屏幕-{version}-debug.apk'
shutil.copyfile(apk, destination / name)
versions = tomllib.loads((source / 'gradle/libs.versions.toml').read_text(encoding='utf-8'))['versions']
build_config = (args.build / 'androidApp/build/generated/source/buildConfig/debug/org/qimiaoscreen/controller/BuildConfig.java').read_text(encoding='utf-8')
build_date = re.search(r'BUILD_DATE = "([^"]+)"', build_config)[1]
manifest = dict(app_version=version, version_code=code, signing='debug', build_date=build_date,
status='development-validation-see-test-report', apk_sha256=digest(apk),
certificate_sha256=certificate, protocol_major=1, minimum_android_api=26,
target_android_api=37, build_tools='36.0.0', toolchain=versions, sources=entries)
(destination / 'manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
(destination / 'SHA256SUMS').write_text(f'{digest(apk)} {name}\n', encoding='utf-8')
(destination / 'README.md').write_text(
f'# 奇妙小屏幕 {version} debug(versionCode {code})\n\n'
'仅 debug 签名,可覆盖安装相同调试证书的旧包。Android 8.0 以上,真实验证机为 Android 13。\n\n'
'本包为当前开发验证产物;验收范围和未验证项见 `../../../测试结果归档/实施状态.md`。'
'不能将 APK 成功生成视为所有首版验收通过。设备需已部署移动控制功能,历史设备版本号本身不代表支持 BLE。\n\n'
f'SHA-256:`{digest(apk)}`。源码摘要与签名证书摘要见 manifest.json。\n', encoding='utf-8')
print(json.dumps(dict(delivery=destination.relative_to(root).as_posix(), apk_sha256=digest(apk), certificate_sha256=certificate), ensure_ascii=False))
if __name__ == '__main__':
main()
@@ -0,0 +1,14 @@
# 奇妙小屏幕 0.1.0 debug 开发验证包
本目录提供当前连接修复阶段 APK,不是全部首版验收完成声明。仅 debug 签名,不包含私钥;可覆盖安装使用同一调试证书的此前测试版本。若提示签名不一致,不自动卸载或清除数据。
- 文件:`奇妙小屏幕-0.1.0-debug.apk`。
- Android 8.0/API 26 及以上;真实验证机为 Android 13。
- BLE 协议主版本 1;须配合本项目已部署移动控制服务的设备开发版本,仅有历史 1.1.1 发布版本号不能证明含 BLE 功能。
- 2026-09-25 修复请求/连接建立超时残留状态,以及厂商缓存 MTU 回调触发过早服务发现的问题。
- 已通过 JVM 6 项、Android lint(0 错误/14 警告)、真实 BLE 60 轮持续读取和 Controller 三页/后台释放/前台重连组合测试(290.908 秒)。实际 Activity 自动化另见测试报告。
- 用户授权 root 输入后,实际 Activity 三页操作、后台 33 秒后返回自动重连及主动断开通过(49.45 秒)。
- 仍需完成完整首版的写操作、网络切换、双手机互斥、设备恢复与性能等验收;不把只读成功当作这些项目通过。
- 详细过程:`../../../测试结果归档/2026-09-25_重启与连接恢复.md`。
SHA-256:`2eedd9fccd27dcc0664e6e0f1003554677b632580d65ae17f95e7ea285a0529e`。同目录 SHA256SUMS 与 manifest.json 包含校验值、调试证书摘要、工具链与源码文件摘要;不包含本机路径或真实设备标识。
@@ -0,0 +1 @@
2eedd9fccd27dcc0664e6e0f1003554677b632580d65ae17f95e7ea285a0529e 奇妙小屏幕-0.1.0-debug.apk
@@ -0,0 +1,128 @@
{
"app_version": "0.1.0",
"version_code": 1,
"status": "development-validation-not-final-release",
"signing": "debug",
"certificate_sha256": "b413e68c0bd2311c491ebd087ac5791c7c2ad8267793f92f6055d5521bd3ef4d",
"apk_sha256": "2eedd9fccd27dcc0664e6e0f1003554677b632580d65ae17f95e7ea285a0529e",
"protocol_major": 1,
"minimum_android_api": 26,
"target_android_api": 37,
"build_tools": "36.0.0",
"gradle": "9.5.0",
"kotlin": "2.4.20",
"compose": "1.12.1",
"agp": "9.3.1",
"device_release_tested": "1.1.1 with mobile development changes",
"date": "2026-09-25",
"sources": [
{
"path": "androidApp/build.gradle.kts",
"sha256": "ed75cde57a6dd389e2a69d3d30ccecb3411e258d2cb36ec057a6f8c9fce20bb6"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt",
"sha256": "e959936e49ad7b90acf04c8f1595d6f497d9b14d6781b13e03eb5914d59ecb1f"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/BleIntegrationTest.kt",
"sha256": "d2236734062ce523f87a9cb7f3149a8a965616bcab3df46e954fdc8ca81f76c0"
},
{
"path": "androidApp/src/main/AndroidManifest.xml",
"sha256": "186ed22b19731ea5bf3567e40d6487b95f1bced3dd2fce605de6557cb7c5763e"
},
{
"path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/AndroidBle.kt",
"sha256": "453c67aab3963c12be915923c88ea1c63930c24e43842c223ca62f421574bc15"
},
{
"path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/MainActivity.kt",
"sha256": "3a688ac321f1f06c9c61be4046cd0ebfbf53893a4742a4aaa9a77aa56bf17838"
},
{
"path": "build.gradle.kts",
"sha256": "feb3b6e64b846f0408cc40e95ae4b632ce9288a56bd6151bc3bbcc46f0549b49"
},
{
"path": "gradle/libs.versions.toml",
"sha256": "ed393bb2b4d81a162f6b3d21968eeca113e23c2f5d5d22c3be9d104bc592a633"
},
{
"path": "gradle/wrapper/gradle-wrapper.jar",
"sha256": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"sha256": "2cb0f9ddf992a26fc4947c91019136bff5e1ccfbf93bd905402b1abf69d15bc5"
},
{
"path": "gradle.properties",
"sha256": "e7d684b7795fd04ab2e7e96038fa2bf4f9fd633578aa3bb2f5fe367b4f5ec159"
},
{
"path": "gradlew",
"sha256": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a"
},
{
"path": "gradlew.bat",
"sha256": "475c4f08cd57cf2faa819e7f36d72aa93f0ad646ea23a8f7fa3ef54dee1cbc52"
},
{
"path": "README.md",
"sha256": "7232ebbac0899152e312429fecaf6b3e14b31265b0c50a95c99a6213aa04b2cd"
},
{
"path": "settings.gradle.kts",
"sha256": "3d53a77b5b92800475897fb55f910a7cfe008e2084e0b7185ebb5ce38e22cf53"
},
{
"path": "sharedCore/build.gradle.kts",
"sha256": "9a420e39e3c655f676572d4486e415dbc8b4fd45904db6ad7b64d7fcd71f084b"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt",
"sha256": "792725610a2f4b802c37c708a8995b647f6482427a89f4df7d005c5b3b9f3026"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/DeviceLink.kt",
"sha256": "8d85e3182e66f7d894f3119329a6bd3d6cdbe8f3ad725f3715dcda477c359220"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Protocol.kt",
"sha256": "99264aff225076ef1b423801b9bba8797e0e06a9e759339bc52e896620e4b694"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/SecureChannel.kt",
"sha256": "bde40388bb74c9d47a4e099a14a0baeca3b6bda1539023dbd7dca831c6f24039"
},
{
"path": "sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ProtocolTest.kt",
"sha256": "5d26d3a50c5887d72dc91b2081273fbe4a5a968f01451ef75ab78903d5d42217"
},
{
"path": "sharedCore/src/javaCryptoMain/kotlin/org/qimiaoscreen/core/JavaSecureChannel.kt",
"sha256": "554d663ceb6ec1be095b31e9ff9549af3ecd02f975059c22ce566c87225b05eb"
},
{
"path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/CryptoInteropTest.kt",
"sha256": "2ddb06c4476952a69bf01c0a3a7164625ae8c0bfe9194349450a2e254888d4b3"
},
{
"path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/RpcTimeoutTest.kt",
"sha256": "fca298fb2266ee7e68e6df76d837f7f191e0046aa5ba321a304c4b4369e064a6"
},
{
"path": "sharedCore/src/jvmTest/resources/protocol-v1.json",
"sha256": "6280953b95a3f5d1ae531d7feaa6a62848ffa3e438e58308d2a27f04723803dc"
},
{
"path": "sharedUi/build.gradle.kts",
"sha256": "d469b330d248647e6e93f98d69eb46730adab329ebaf71cbfd896d5426fd0ff1"
},
{
"path": "sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt",
"sha256": "b9a9e0f95d301d35e19424c5a6342e0157292a77044126da908341af89ea6954"
}
]
}
@@ -0,0 +1,9 @@
# 奇妙小屏幕 0.1.0 debug(versionCode 2)
仅 debug 签名,可覆盖安装相同调试证书的旧包。Android 8.0 以上,真实验证机为 Android 13。
本包为当前开发验证产物;验收范围和未验证项见 `../../../测试结果归档/实施状态.md`。不能将 APK 成功生成视为所有首版验收通过。设备需已部署移动控制功能,历史设备版本号本身不代表支持 BLE。
SHA-256:`f7c1dc215aa5934359a37fd001e1bc3cbf959b4a386662b1a886efb1ab73ea46`。源码摘要与签名证书摘要见 manifest.json。
本版实际 Activity 三页、蓝牙切换、后台释放及重连测试通过(60.105 秒)。完整通过/未验证/不适用与恢复说明见 `../../../测试结果归档/2026-09-25_本轮交付核对.md`。
@@ -0,0 +1 @@
f7c1dc215aa5934359a37fd001e1bc3cbf959b4a386662b1a886efb1ab73ea46 奇妙小屏幕-0.1.0-debug.apk
@@ -0,0 +1,133 @@
{
"app_version": "0.1.0",
"version_code": 2,
"signing": "debug",
"status": "development-validation-see-test-report",
"apk_sha256": "f7c1dc215aa5934359a37fd001e1bc3cbf959b4a386662b1a886efb1ab73ea46",
"certificate_sha256": "b413e68c0bd2311c491ebd087ac5791c7c2ad8267793f92f6055d5521bd3ef4d",
"protocol_major": 1,
"minimum_android_api": 26,
"target_android_api": 37,
"build_tools": "36.0.0",
"toolchain": {
"kotlin": "2.4.20",
"agp": "9.3.1",
"compose": "1.12.1",
"coroutines": "1.10.2",
"serialization": "1.9.0"
},
"sources": [
{
"path": "androidApp/build.gradle.kts",
"sha256": "c786ed104d02a545a1824c6d3e4b4ba4d7eda0f8cdb2aacc90c7ac0203482457"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt",
"sha256": "128df8ba46bea5c965118d6b76156389fd9d6face6129f627788ecc312383a0c"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/BleIntegrationTest.kt",
"sha256": "db8149886b54d896148b2f07cc77dfec6906ada31a6aae54421272c0d66c276c"
},
{
"path": "androidApp/src/main/AndroidManifest.xml",
"sha256": "186ed22b19731ea5bf3567e40d6487b95f1bced3dd2fce605de6557cb7c5763e"
},
{
"path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/AndroidBle.kt",
"sha256": "453c67aab3963c12be915923c88ea1c63930c24e43842c223ca62f421574bc15"
},
{
"path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/MainActivity.kt",
"sha256": "d79523c52eee42041a4e14ba664d08a9a614dc79f9578b3a6ab1f8db7268edd4"
},
{
"path": "build.gradle.kts",
"sha256": "feb3b6e64b846f0408cc40e95ae4b632ce9288a56bd6151bc3bbcc46f0549b49"
},
{
"path": "gradle/libs.versions.toml",
"sha256": "ed393bb2b4d81a162f6b3d21968eeca113e23c2f5d5d22c3be9d104bc592a633"
},
{
"path": "gradle/wrapper/gradle-wrapper.jar",
"sha256": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"sha256": "2cb0f9ddf992a26fc4947c91019136bff5e1ccfbf93bd905402b1abf69d15bc5"
},
{
"path": "gradle.properties",
"sha256": "e7d684b7795fd04ab2e7e96038fa2bf4f9fd633578aa3bb2f5fe367b4f5ec159"
},
{
"path": "gradlew",
"sha256": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a"
},
{
"path": "gradlew.bat",
"sha256": "475c4f08cd57cf2faa819e7f36d72aa93f0ad646ea23a8f7fa3ef54dee1cbc52"
},
{
"path": "README.md",
"sha256": "7232ebbac0899152e312429fecaf6b3e14b31265b0c50a95c99a6213aa04b2cd"
},
{
"path": "settings.gradle.kts",
"sha256": "3d53a77b5b92800475897fb55f910a7cfe008e2084e0b7185ebb5ce38e22cf53"
},
{
"path": "sharedCore/build.gradle.kts",
"sha256": "9a420e39e3c655f676572d4486e415dbc8b4fd45904db6ad7b64d7fcd71f084b"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt",
"sha256": "1b23010730aee35f2512bb247a79040360a046a055b7029e269a17f9790686e7"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/DeviceLink.kt",
"sha256": "8d85e3182e66f7d894f3119329a6bd3d6cdbe8f3ad725f3715dcda477c359220"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Protocol.kt",
"sha256": "99264aff225076ef1b423801b9bba8797e0e06a9e759339bc52e896620e4b694"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/SecureChannel.kt",
"sha256": "bde40388bb74c9d47a4e099a14a0baeca3b6bda1539023dbd7dca831c6f24039"
},
{
"path": "sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ControllerRecoveryTest.kt",
"sha256": "4107b56cd9a30009aa9b16ebb22e349e8a46134be1cb49078a81e1d8b5e7412b"
},
{
"path": "sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ProtocolTest.kt",
"sha256": "5d26d3a50c5887d72dc91b2081273fbe4a5a968f01451ef75ab78903d5d42217"
},
{
"path": "sharedCore/src/javaCryptoMain/kotlin/org/qimiaoscreen/core/JavaSecureChannel.kt",
"sha256": "554d663ceb6ec1be095b31e9ff9549af3ecd02f975059c22ce566c87225b05eb"
},
{
"path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/CryptoInteropTest.kt",
"sha256": "2ddb06c4476952a69bf01c0a3a7164625ae8c0bfe9194349450a2e254888d4b3"
},
{
"path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/RpcTimeoutTest.kt",
"sha256": "fca298fb2266ee7e68e6df76d837f7f191e0046aa5ba321a304c4b4369e064a6"
},
{
"path": "sharedCore/src/jvmTest/resources/protocol-v1.json",
"sha256": "6280953b95a3f5d1ae531d7feaa6a62848ffa3e438e58308d2a27f04723803dc"
},
{
"path": "sharedUi/build.gradle.kts",
"sha256": "d469b330d248647e6e93f98d69eb46730adab329ebaf71cbfd896d5426fd0ff1"
},
{
"path": "sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt",
"sha256": "4c9f1e67aca84ed025110cd36daf2a20ee1da7fbff7f525a2eed0d4203fb2819"
}
]
}
@@ -0,0 +1,13 @@
# 奇妙小屏幕 0.2.0 debug(versionCode 3)
仅 debug 签名,可覆盖安装相同调试证书的旧包。Android 8.0 以上,真实验证机为 Android 13。
本包为当前开发验证产物;验收范围和未验证项见 `../../../测试结果归档/实施状态.md`。不能将 APK 成功生成视为所有首版验收通过。设备需已部署移动控制功能,历史设备版本号本身不代表支持 BLE。
SHA-256:`9adc8e572384bb30a8a78115be43a8fefeefee6d25bda0136dff106e2a8bcab3`。源码摘要与签名证书摘要见 manifest.json。
## 本轮验证
四栏、菜单、设备记忆/重命名、后台重连、窄屏/横屏/大字体导航、301.514 秒完整 BLE 回归、WiFi 扫描填入及明确错误密码反馈、原网络恢复均在登记 root 手机验证通过。测试手机已覆盖安装本包,已安装 APK 摘要与交付文件一致。
真实双设备切换、旧 Android、跨 SSID 和实际 DHCP/静态互换未验证;不把模拟和表单测试等同于这些真机验收。详细证据与失败记录见 `../../../测试结果归档/2026-09-26_四栏界面优化.md`。
@@ -0,0 +1 @@
9adc8e572384bb30a8a78115be43a8fefeefee6d25bda0136dff106e2a8bcab3 奇妙小屏幕-0.2.0-debug.apk
@@ -0,0 +1,142 @@
{
"app_version": "0.2.0",
"version_code": 3,
"signing": "debug",
"build_date": "2026-09-26 15:13 +08:00",
"status": "development-validation-see-test-report",
"apk_sha256": "9adc8e572384bb30a8a78115be43a8fefeefee6d25bda0136dff106e2a8bcab3",
"certificate_sha256": "b413e68c0bd2311c491ebd087ac5791c7c2ad8267793f92f6055d5521bd3ef4d",
"protocol_major": 1,
"minimum_android_api": 26,
"target_android_api": 37,
"build_tools": "36.0.0",
"toolchain": {
"kotlin": "2.4.20",
"agp": "9.3.1",
"compose": "1.12.1",
"coroutines": "1.10.2",
"serialization": "1.9.0"
},
"sources": [
{
"path": "androidApp/build.gradle.kts",
"sha256": "45aeaaf7cff436d7dc93e6c6f1491925b082d989bcd3b097fb5a4052e8bbe8c7"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/AppLifecycleTest.kt",
"sha256": "ba33ea5102809e6b0b5b594159f05adfc612b02a0430e329a0620b20c76cb21f"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/BleIntegrationTest.kt",
"sha256": "8dc7a28279e99e46d15c914955d8974d619c4e48a2fcab887f180b15b4b183ab"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/LayoutTest.kt",
"sha256": "930c964fab8975d681e6d698468146294653af4d66a4a7be5c162e4c14659696"
},
{
"path": "androidApp/src/androidTest/kotlin/org/qimiaoscreen/controller/WifiSwitchTest.kt",
"sha256": "5ce335ff84fee01230733971964fefde3fcc6103172d3c40fee64f50838b7bcf"
},
{
"path": "androidApp/src/main/AndroidManifest.xml",
"sha256": "186ed22b19731ea5bf3567e40d6487b95f1bced3dd2fce605de6557cb7c5763e"
},
{
"path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/AndroidBle.kt",
"sha256": "a1af9d6593c3be1af0f2a39a3869eae2e1bbbb1dab8b775e06da5dcaa739e9ff"
},
{
"path": "androidApp/src/main/kotlin/org/qimiaoscreen/controller/MainActivity.kt",
"sha256": "7bece662b79093a19fcdf34ad344cf42a57b60291f0536823395ae06663f4f23"
},
{
"path": "build.gradle.kts",
"sha256": "feb3b6e64b846f0408cc40e95ae4b632ce9288a56bd6151bc3bbcc46f0549b49"
},
{
"path": "gradle/libs.versions.toml",
"sha256": "ed393bb2b4d81a162f6b3d21968eeca113e23c2f5d5d22c3be9d104bc592a633"
},
{
"path": "gradle/wrapper/gradle-wrapper.jar",
"sha256": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"sha256": "2cb0f9ddf992a26fc4947c91019136bff5e1ccfbf93bd905402b1abf69d15bc5"
},
{
"path": "gradle.properties",
"sha256": "e7d684b7795fd04ab2e7e96038fa2bf4f9fd633578aa3bb2f5fe367b4f5ec159"
},
{
"path": "gradlew",
"sha256": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a"
},
{
"path": "gradlew.bat",
"sha256": "475c4f08cd57cf2faa819e7f36d72aa93f0ad646ea23a8f7fa3ef54dee1cbc52"
},
{
"path": "README.md",
"sha256": "7232ebbac0899152e312429fecaf6b3e14b31265b0c50a95c99a6213aa04b2cd"
},
{
"path": "settings.gradle.kts",
"sha256": "3d53a77b5b92800475897fb55f910a7cfe008e2084e0b7185ebb5ce38e22cf53"
},
{
"path": "sharedCore/build.gradle.kts",
"sha256": "9a420e39e3c655f676572d4486e415dbc8b4fd45904db6ad7b64d7fcd71f084b"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Controller.kt",
"sha256": "c368f57d13ba84bcb458855465ec22843d8ed617c7242bf2d5a3b4730271dca3"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/DeviceLink.kt",
"sha256": "870345382351bbf6d37f4332c0fab099cb47b7b703428a98567a0086c3316be6"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/Protocol.kt",
"sha256": "99264aff225076ef1b423801b9bba8797e0e06a9e759339bc52e896620e4b694"
},
{
"path": "sharedCore/src/commonMain/kotlin/org/qimiaoscreen/core/SecureChannel.kt",
"sha256": "bde40388bb74c9d47a4e099a14a0baeca3b6bda1539023dbd7dca831c6f24039"
},
{
"path": "sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ControllerRecoveryTest.kt",
"sha256": "a4a4c3e2ba64b0facc1e1c491d440f91e4bdcf53eb87d9991921f4f568614f58"
},
{
"path": "sharedCore/src/commonTest/kotlin/org/qimiaoscreen/core/ProtocolTest.kt",
"sha256": "5d26d3a50c5887d72dc91b2081273fbe4a5a968f01451ef75ab78903d5d42217"
},
{
"path": "sharedCore/src/javaCryptoMain/kotlin/org/qimiaoscreen/core/JavaSecureChannel.kt",
"sha256": "554d663ceb6ec1be095b31e9ff9549af3ecd02f975059c22ce566c87225b05eb"
},
{
"path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/CryptoInteropTest.kt",
"sha256": "2ddb06c4476952a69bf01c0a3a7164625ae8c0bfe9194349450a2e254888d4b3"
},
{
"path": "sharedCore/src/jvmTest/kotlin/org/qimiaoscreen/core/RpcTimeoutTest.kt",
"sha256": "fca298fb2266ee7e68e6df76d837f7f191e0046aa5ba321a304c4b4369e064a6"
},
{
"path": "sharedCore/src/jvmTest/resources/protocol-v1.json",
"sha256": "6280953b95a3f5d1ae531d7feaa6a62848ffa3e438e58308d2a27f04723803dc"
},
{
"path": "sharedUi/build.gradle.kts",
"sha256": "d469b330d248647e6e93f98d69eb46730adab329ebaf71cbfd896d5426fd0ff1"
},
{
"path": "sharedUi/src/commonMain/kotlin/org/qimiaoscreen/ui/ControllerApp.kt",
"sha256": "0f88692bb155d68baaadbad3beec5443975a7863b7a1ceb6733958aab064d132"
}
]
}