mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-21 03:40:40 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
852710f428 | ||
|
|
2d51936ca8 | ||
|
|
3baedac6df | ||
|
|
a667fe3b31 | ||
|
|
05f6d3d90a | ||
|
|
0e7d175631 | ||
|
|
52ca253980 | ||
|
|
1925e5ffcc | ||
|
|
3aa0470510 | ||
|
|
fbbb8c9e2c | ||
|
|
3750687e66 |
@@ -2,6 +2,12 @@ name: Build Android
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
patch:
|
||||||
|
description: "Optional patch number, matching the main build workflow."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "0"
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
source_tag:
|
source_tag:
|
||||||
@@ -9,9 +15,14 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: ""
|
default: ""
|
||||||
|
patch:
|
||||||
|
description: "Optional patch number from the main build workflow."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "0"
|
||||||
outputs:
|
outputs:
|
||||||
version:
|
version:
|
||||||
description: "Independent Android version and build date."
|
description: "Android artifact version; combined releases match the desktop tag."
|
||||||
value: ${{ jobs.build.outputs.version }}
|
value: ${{ jobs.build.outputs.version }}
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
@@ -147,51 +158,70 @@ jobs:
|
|||||||
- name: Verify shared license catalog
|
- name: Verify shared license catalog
|
||||||
run: python3 -B apps/ios/scripts/generate_third_party_licenses.py
|
run: python3 -B apps/ios/scripts/generate_third_party_licenses.py
|
||||||
|
|
||||||
|
- name: Resolve Android version
|
||||||
|
shell: python
|
||||||
|
env:
|
||||||
|
PATCH_NUMBER: ${{ inputs.patch || '0' }}
|
||||||
|
run: |
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import runpy
|
||||||
|
|
||||||
|
resolver = Path("apps/android/scripts/release_version.py")
|
||||||
|
if resolver.exists():
|
||||||
|
runpy.run_path(str(resolver), run_name="__main__")
|
||||||
|
else:
|
||||||
|
# Older standalone releases predate the resolver and Gradle properties.
|
||||||
|
# Keep their original package version when retrying an existing tag.
|
||||||
|
tag = os.environ["CROSSDESK_SOURCE_TAG"]
|
||||||
|
match = re.fullmatch(r"android-v([0-9]+\.[0-9]+\.[0-9]+)-([0-9]{8})", tag)
|
||||||
|
config = Path("apps/android/app/build.gradle").read_text()
|
||||||
|
name = re.search(r"versionName\s+'([^']+)'", config)
|
||||||
|
code = re.search(r"versionCode\s+([0-9]+)", config)
|
||||||
|
if not match or not name or not code or match[1] != name[1]:
|
||||||
|
raise ValueError("Legacy Android sources require a matching standalone release tag")
|
||||||
|
datetime.datetime.strptime(match[2], "%Y%m%d")
|
||||||
|
with open(os.environ["GITHUB_ENV"], "a") as stream:
|
||||||
|
stream.write(f"ANDROID_VERSION_NAME={name[1]}\nANDROID_VERSION_CODE={code[1]}\n"
|
||||||
|
f"ANDROID_ARTIFACT_VERSION={tag.removeprefix('android-')}\n")
|
||||||
|
|
||||||
- name: Build APKs
|
- name: Build APKs
|
||||||
working-directory: apps/android
|
working-directory: apps/android
|
||||||
run: |
|
run: |
|
||||||
./gradlew --no-daemon --stacktrace \
|
./gradlew --no-daemon --stacktrace \
|
||||||
|
-PcrossdeskVersionName="${ANDROID_VERSION_NAME}" \
|
||||||
|
-PcrossdeskVersionCode="${ANDROID_VERSION_CODE}" \
|
||||||
:app:assembleDebug :app:assembleRelease
|
:app:assembleDebug :app:assembleRelease
|
||||||
|
|
||||||
- name: Verify APKs
|
- name: Verify APKs
|
||||||
working-directory: apps/android
|
working-directory: apps/android
|
||||||
run: |
|
run: |
|
||||||
python3 -B test/verify_apk.py app/build/outputs/apk/debug/app-debug.apk
|
for APK in app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/release/app-release-unsigned.apk; do
|
||||||
python3 -B test/verify_apk.py app/build/outputs/apk/release/app-release-unsigned.apk
|
python3 -B test/verify_apk.py "${APK}" \
|
||||||
|
--aapt2 "${ANDROID_HOME}/build-tools/36.0.0/aapt2" \
|
||||||
|
--version-name "${ANDROID_VERSION_NAME}" --version-code "${ANDROID_VERSION_CODE}"
|
||||||
|
done
|
||||||
"${ANDROID_HOME}/build-tools/36.0.0/apksigner" verify app/build/outputs/apk/debug/app-debug.apk
|
"${ANDROID_HOME}/build-tools/36.0.0/apksigner" verify app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
- name: Package APKs with the independent Android version
|
- name: Package APKs with the resolved Android version
|
||||||
id: package
|
id: package
|
||||||
shell: python
|
shell: python
|
||||||
run: |
|
run: |
|
||||||
import datetime
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
apk_dir = Path("apps/android/app/build/outputs/apk")
|
apk_dir = Path("apps/android/app/build/outputs/apk")
|
||||||
metadata = json.loads((apk_dir / "release/output-metadata.json").read_text())
|
version = os.environ["ANDROID_ARTIFACT_VERSION"]
|
||||||
version = metadata["elements"][0]["versionName"]
|
for variant in ("debug", "release"):
|
||||||
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version):
|
metadata = json.loads((apk_dir / variant / "output-metadata.json").read_text())
|
||||||
raise ValueError(f"Invalid Android version: {version!r}")
|
for element in metadata["elements"]:
|
||||||
build_date = datetime.datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y%m%d")
|
if (element["versionName"] != os.environ["ANDROID_VERSION_NAME"] or
|
||||||
# Match the dated release tags used by the desktop and iOS jobs.
|
element["versionCode"] != int(os.environ["ANDROID_VERSION_CODE"])):
|
||||||
tag = os.environ.get("CROSSDESK_SOURCE_TAG")
|
raise ValueError(f"Unexpected Android {variant} version: {element}")
|
||||||
if not tag and os.environ["GITHUB_REF_TYPE"] == "tag":
|
|
||||||
tag = os.environ["GITHUB_REF_NAME"]
|
|
||||||
if tag:
|
|
||||||
tag = tag.removeprefix("android-")
|
|
||||||
match = re.fullmatch(r"v?[0-9]+(?:\.[0-9]+){1,3}-(?:[0-9]+-)?([0-9]{8})(?:-[0-9]+)?", tag)
|
|
||||||
if match:
|
|
||||||
build_date = match[1]
|
|
||||||
datetime.datetime.strptime(build_date, "%Y%m%d")
|
|
||||||
version = f"v{version}-{build_date}"
|
|
||||||
source_tag = os.environ.get("CROSSDESK_SOURCE_TAG", "")
|
|
||||||
if source_tag.startswith("android-v") and source_tag.removeprefix("android-") != version:
|
|
||||||
raise ValueError(f"Android source tag does not match the app version: {source_tag} vs {version}")
|
|
||||||
output = Path(os.environ["RUNNER_TEMP"]) / "crossdesk-android"
|
output = Path(os.environ["RUNNER_TEMP"]) / "crossdesk-android"
|
||||||
for variant, source in (
|
for variant, source in (
|
||||||
("unsigned", "release/app-release-unsigned.apk"),
|
("unsigned", "release/app-release-unsigned.apk"),
|
||||||
|
|||||||
@@ -346,6 +346,8 @@ jobs:
|
|||||||
build-android:
|
build-android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
uses: ./.github/workflows/build-android.yml
|
uses: ./.github/workflows/build-android.yml
|
||||||
|
with:
|
||||||
|
patch: ${{ inputs.patch || '0' }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CrossDesk Android
|
# CrossDesk Android
|
||||||
|
|
||||||
原生 Android 控制端,使用 Java 界面、JNI 和 MiniRTC,与桌面端使用相同的信令、ICE、SRTP 和输入协议。没有 WebView。当前版本为 1.6.0,支持 Android 8.0(API 26)及以上的 arm64 手机和平板。
|
原生 Android 控制端,使用 Java 界面、JNI 和 MiniRTC,与桌面端使用相同的信令、ICE、SRTP 和输入协议。没有 WebView。当前版本为 1.6.1,支持 Android 8.0(API 26)及以上的 arm64 手机和平板。
|
||||||
|
|
||||||
## 当前功能
|
## 当前功能
|
||||||
|
|
||||||
@@ -83,11 +83,11 @@ Release 构建使用 `./gradlew :app:assembleRelease`,默认输出未签名 AP
|
|||||||
|
|
||||||
[`Build Android`](../../.github/workflows/build-android.yml) 可在 Actions 页面单独手动运行;修改 Android、MiniRTC、共享协议或许可证资料的 Pull Request 也会触发。仓库原有的 [`Build and Release`](../../.github/workflows/build.yml) 在分支推送、标签推送及手动运行时调用同一工作流,`v` 标签发布会等待安卓构建及签名成功,并将已签名 Release APK 加入 GitHub Release 和现有下载服务器的产物集合。推送主仓库前,先确保它引用的 MiniRTC 提交已推送到子模块远程仓库,否则 CI 无法完成检出。
|
[`Build Android`](../../.github/workflows/build-android.yml) 可在 Actions 页面单独手动运行;修改 Android、MiniRTC、共享协议或许可证资料的 Pull Request 也会触发。仓库原有的 [`Build and Release`](../../.github/workflows/build.yml) 在分支推送、标签推送及手动运行时调用同一工作流,`v` 标签发布会等待安卓构建及签名成功,并将已签名 Release APK 加入 GitHub Release 和现有下载服务器的产物集合。推送主仓库前,先确保它引用的 MiniRTC 提交已推送到子模块远程仓库,否则 CI 无法完成检出。
|
||||||
|
|
||||||
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先提交 `app/build.gradle` 中需要发布的 `versionName`,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先更新并提交 `gradle.properties` 中的 `crossdeskVersionName` 和递增的 `crossdeskVersionCode`,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git tag android-v1.6.0-20261002
|
git tag android-v1.6.1-20261010
|
||||||
git push origin android-v1.6.0-20261002
|
git push origin android-v1.6.1-20261010
|
||||||
```
|
```
|
||||||
|
|
||||||
该标签只触发安卓构建,使用固定发布密钥签名并验证 Release APK 后,再发布到独立的 GitHub Release 和现有下载服务器。签名失败或缺少签名 Secrets 时停止发布,不回退到未签名 APK。标签版本必须与应用版本一致,日期必须有效。重试发布时,在 Actions 页面手动运行 `Release Mobile`,将已有标签填入 `source_tag`;即使从其他分支触发,也会检出该标签的源码。APK 的源码资料记录安卓标签及准确的应用、MiniRTC 提交。
|
该标签只触发安卓构建,使用固定发布密钥签名并验证 Release APK 后,再发布到独立的 GitHub Release 和现有下载服务器。签名失败或缺少签名 Secrets 时停止发布,不回退到未签名 APK。标签版本必须与应用版本一致,日期必须有效。重试发布时,在 Actions 页面手动运行 `Release Mobile`,将已有标签填入 `source_tag`;即使从其他分支触发,也会检出该标签的源码。APK 的源码资料记录安卓标签及准确的应用、MiniRTC 提交。
|
||||||
@@ -100,15 +100,15 @@ git push origin android-v1.6.0-20261002
|
|||||||
|
|
||||||
Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。GitHub 托管 runner 每次启动仍是新环境;第一次运行或缓存失效时需要安装。不同分支和标签可读取默认分支的缓存,因此建议先在默认分支运行一次 `Build Android` 预热,再创建发布标签。
|
Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。GitHub 托管 runner 每次启动仍是新环境;第一次运行或缓存失效时需要安装。不同分支和标签可读取默认分支的缓存,因此建议先在默认分支运行一次 `Build Android` 预热,再创建发布标签。
|
||||||
|
|
||||||
CI 仅构建和打包 Debug、Release APK,并校验 APK 的 arm64 ABI、16 KB ELF / ZIP 对齐、完整许可证及 Debug 签名。单元测试、本机 C++ 协议与画面队列测试、Lint 和设备测试改为按需在本地执行;设备测试使用下面的设备验证流程在 arm64 手机或模拟器上执行。
|
CI 仅构建和打包 Debug、Release APK,并校验 APK 的包名、`versionName` / `versionCode`、arm64 ABI、16 KB ELF / ZIP 对齐、完整许可证及 Debug 签名。单元测试、本机 C++ 协议与画面队列测试、Lint 和设备测试改为按需在本地执行;设备测试使用下面的设备验证流程在 arm64 手机或模拟器上执行。
|
||||||
|
|
||||||
产物使用 Android 工程自己的 `versionName`,例如 `v1.6.0-20261002`,不跟随桌面版本号。构建日期采用上海时区;带日期的版本标签沿用标签中的日期,与 iOS 一致。Actions 提供:
|
统一 `v*` 发布从标签提取数字版本,通过 Gradle 属性写入 APK 的 `versionName`;App 内版本展示读取同一包信息。产物版本、日期及可选热修复编号与桌面端保持一致,例如 `v1.6.1-20261010` 或 `v1.6.1-2-20261010`。分支构建和独立 `android-v*` 发布使用 `gradle.properties` 中的版本,独立发布会检查标签与配置一致。构建日期采用上海时区;带日期的版本标签沿用标签中的日期,与 iOS 一致。Actions 提供:
|
||||||
|
|
||||||
- `crossdesk-android-arm64-<版本>.apk`:已签名 Release APK,独立安卓发布和全平台标签发布收集此 APK,可直接安装。
|
- `crossdesk-android-arm64-<版本>.apk`:已签名 Release APK,独立安卓发布和全平台标签发布收集此 APK,可直接安装。
|
||||||
- `crossdesk-android-arm64-unsigned-<版本>.apk`:构建工作流的中间产物,也可用于自行签名和验证修改后的构建。
|
- `crossdesk-android-arm64-unsigned-<版本>.apk`:构建工作流的中间产物,也可用于自行签名和验证修改后的构建。
|
||||||
- `crossdesk-android-arm64-debug-<版本>.apk`:可安装的调试应用,保留 14 天。
|
- `crossdesk-android-arm64-debug-<版本>.apk`:可安装的调试应用,保留 14 天。
|
||||||
|
|
||||||
Debug APK 使用每次运行的临时调试证书,可能无法覆盖手机上已有的其他签名版本;卸载原应用会清除本机连接记录和密码。需要持续覆盖升级时,应使用同一私钥签名 Release APK,并在发布新版本时递增 `app/build.gradle` 中的 `versionCode`。未签名 APK 已完成对齐,可通过 SDK 的 `apksigner sign --ks <自己的密钥库> --out <已签名.apk> <未签名.apk>` 签名,再用 `apksigner verify` 检查。
|
Debug APK 使用每次运行的临时调试证书,可能无法覆盖手机上已有的其他签名版本;卸载原应用会清除本机连接记录和密码。需要持续覆盖升级时,应使用同一私钥签名 Release APK,并在每次发布新版本(含统一发布及热修复)时递增 `gradle.properties` 中的 `crossdeskVersionCode`。该安装升级序号独立于显示版本,不从日期或 CI 运行编号推算。未签名 APK 已完成对齐,可通过 SDK 的 `apksigner sign --ks <自己的密钥库> --out <已签名.apk> <未签名.apk>` 签名,再用 `apksigner verify` 检查。
|
||||||
|
|
||||||
### Release 签名
|
### Release 签名
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ android {
|
|||||||
applicationId 'cn.crossdesk.mobile'
|
applicationId 'cn.crossdesk.mobile'
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 2
|
versionCode providers.gradleProperty('crossdeskVersionCode').get().toInteger()
|
||||||
versionName '1.6.0'
|
versionName providers.gradleProperty('crossdeskVersionName').get()
|
||||||
ndk { abiFilters 'arm64-v8a' }
|
ndk { abiFilters 'arm64-v8a' }
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
crossdeskVersionName=1.6.1
|
||||||
|
crossdeskVersionCode=3
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Resolve Android package and artifact versions before building the APKs."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_version(version_name, version_code, tag="", patch="0", today=None):
|
||||||
|
build_date = today or datetime.datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y%m%d")
|
||||||
|
patch = patch if re.fullmatch(r"[0-9]+", patch) else "0"
|
||||||
|
if tag:
|
||||||
|
independent = tag.startswith("android-v")
|
||||||
|
source_version = tag.removeprefix("android-")
|
||||||
|
base = r"(?P<base>[0-9]+\.[0-9]+\.[0-9]+)"
|
||||||
|
for pattern in (
|
||||||
|
rf"v{base}-(?P<patch>[0-9]+)-(?P<date>[0-9]{{8}})",
|
||||||
|
rf"v{base}-(?P<date>[0-9]{{8}})(?:-(?P<patch>[0-9]+))?",
|
||||||
|
rf"v{base}(?:-(?P<patch>[0-9]+))?",
|
||||||
|
):
|
||||||
|
match = re.fullmatch(pattern, source_version)
|
||||||
|
if match:
|
||||||
|
fields = match.groupdict()
|
||||||
|
if independent and fields["base"] != version_name:
|
||||||
|
raise ValueError(f"Android source tag does not match the app version: {tag} vs {version_name}")
|
||||||
|
version_name = fields["base"]
|
||||||
|
build_date = fields.get("date") or build_date
|
||||||
|
patch = fields.get("patch") or patch
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid Android release tag: {tag!r}")
|
||||||
|
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version_name):
|
||||||
|
raise ValueError(f"Invalid Android versionName: {version_name!r}")
|
||||||
|
if not re.fullmatch(r"[0-9]+", version_code) or not 0 < int(version_code) <= 2100000000:
|
||||||
|
raise ValueError(f"Invalid Android versionCode: {version_code!r}")
|
||||||
|
datetime.datetime.strptime(build_date, "%Y%m%d")
|
||||||
|
suffix = f"-{patch}" if patch != "0" else ""
|
||||||
|
artifact_version = f"v{version_name}{suffix}-{build_date}"
|
||||||
|
if tag.startswith("android-v") and tag.removeprefix("android-") != artifact_version:
|
||||||
|
raise ValueError(f"Android source tag does not match the artifact version: {tag} vs {artifact_version}")
|
||||||
|
return {"ANDROID_VERSION_NAME": version_name, "ANDROID_VERSION_CODE": str(int(version_code)),
|
||||||
|
"ANDROID_ARTIFACT_VERSION": artifact_version}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
properties = dict(line.split("=", 1) for line in
|
||||||
|
(Path(__file__).resolve().parents[1] / "gradle.properties").read_text().splitlines()
|
||||||
|
if "=" in line and not line.lstrip().startswith("#"))
|
||||||
|
tag = os.environ.get("CROSSDESK_SOURCE_TAG", "")
|
||||||
|
if not tag and os.environ.get("GITHUB_REF_TYPE") == "tag":
|
||||||
|
ref_name = os.environ["GITHUB_REF_NAME"]
|
||||||
|
# Moving aliases such as "latest" are builds, not versioned releases.
|
||||||
|
if ref_name.startswith(("v", "android-v")):
|
||||||
|
tag = ref_name
|
||||||
|
values = resolve_version(properties["crossdeskVersionName"], properties["crossdeskVersionCode"],
|
||||||
|
tag, os.environ.get("PATCH_NUMBER", "0"))
|
||||||
|
text = "".join(f"{key}={value}\n" for key, value in values.items())
|
||||||
|
if os.environ.get("GITHUB_ENV"):
|
||||||
|
with open(os.environ["GITHUB_ENV"], "a") as stream:
|
||||||
|
stream.write(text)
|
||||||
|
print(text, end="")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Check the shipped native ABI, ELF load alignment and APK zip alignment."""
|
"""Check the shipped native ABI, ELF load alignment and APK zip alignment."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import subprocess
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
def verify_package_version(badging, version_name, version_code):
|
||||||
|
package = next((line for line in badging.splitlines() if line.startswith('package: ')), '')
|
||||||
|
attributes = dict(re.findall(r"(\w+)='([^']*)'", package))
|
||||||
|
expected = {'name': 'cn.crossdesk.mobile', 'versionName': version_name, 'versionCode': str(version_code)}
|
||||||
|
for key, value in expected.items():
|
||||||
|
if attributes.get(key) != value:
|
||||||
|
raise ValueError(f'Unexpected APK {key}: expected {value!r}, got {attributes.get(key)!r}')
|
||||||
|
|
||||||
|
|
||||||
def verify(path):
|
def verify(path):
|
||||||
with zipfile.ZipFile(path) as archive, open(path, 'rb') as raw:
|
with zipfile.ZipFile(path) as archive, open(path, 'rb') as raw:
|
||||||
libraries = [i for i in archive.infolist() if i.filename.endswith('.so')]
|
libraries = [i for i in archive.infolist() if i.filename.endswith('.so')]
|
||||||
@@ -59,4 +70,16 @@ def verify(path):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
verify(sys.argv[1])
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('apk', type=Path)
|
||||||
|
parser.add_argument('--aapt2', type=Path)
|
||||||
|
parser.add_argument('--version-name')
|
||||||
|
parser.add_argument('--version-code', type=int)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if any(value is not None for value in (args.aapt2, args.version_name, args.version_code)):
|
||||||
|
if any(value is None for value in (args.aapt2, args.version_name, args.version_code)):
|
||||||
|
parser.error('--aapt2, --version-name and --version-code must be supplied together')
|
||||||
|
badging = subprocess.check_output([str(args.aapt2), 'dump', 'badging', str(args.apk)], text=True)
|
||||||
|
verify_package_version(badging, args.version_name, args.version_code)
|
||||||
|
print(f'PASS: {args.apk.name}: versionName={args.version_name}, versionCode={args.version_code}')
|
||||||
|
verify(args.apk)
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ struct ServerWindowState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct UiState {
|
struct UiState {
|
||||||
bool password_validating_ = false;
|
|
||||||
uint32_t password_validating_time_ = 0;
|
|
||||||
bool rejoin_ = false;
|
bool rejoin_ = false;
|
||||||
bool show_password_ = true;
|
bool show_password_ = true;
|
||||||
bool show_connection_status_window_ = false;
|
bool show_connection_status_window_ = false;
|
||||||
|
|||||||
@@ -1363,6 +1363,12 @@ void GuiApplication::BindMainCallbacks() {
|
|||||||
LOG_WARN("Copy local id failed: {}", SDL_GetError());
|
LOG_WARN("Copy local id failed: {}", SDL_GetError());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
main->on_signal_reconnect_requested([this] {
|
||||||
|
LOG_INFO("Signal reconnect requested from the status bar");
|
||||||
|
if (ForceSignalReconnect() != 0) {
|
||||||
|
LOG_WARN("Signal reconnect could not be started");
|
||||||
|
}
|
||||||
|
});
|
||||||
main->on_toggle_password_visibility([this] {
|
main->on_toggle_password_visibility([this] {
|
||||||
show_password_ = !show_password_;
|
show_password_ = !show_password_;
|
||||||
ui_->main->set_password_visible(show_password_);
|
ui_->main->set_password_visible(show_password_);
|
||||||
@@ -1451,14 +1457,8 @@ void GuiApplication::BindMainCallbacks() {
|
|||||||
show_connection_status_window_ = false;
|
show_connection_status_window_ = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ConnectionStatus status = props->connection_status_.load();
|
CloseStreamTab(props->remote_id_);
|
||||||
if (status == ConnectionStatus::IncorrectPassword) {
|
re_enter_remote_id_ = true;
|
||||||
std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
|
|
||||||
password_validating_ = false;
|
|
||||||
} else {
|
|
||||||
CloseStreamTab(props->remote_id_);
|
|
||||||
re_enter_remote_id_ = true;
|
|
||||||
}
|
|
||||||
show_connection_status_window_ = false;
|
show_connection_status_window_ = false;
|
||||||
});
|
});
|
||||||
main->on_connection_acknowledge([this] {
|
main->on_connection_acknowledge([this] {
|
||||||
@@ -1480,11 +1480,11 @@ void GuiApplication::BindMainCallbacks() {
|
|||||||
if (!props || password.empty() || password.size() > 6) {
|
if (!props || password.empty() || password.size() > 6) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!props->connection_auth_.Begin(password)) return;
|
||||||
std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
|
std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
|
||||||
std::memcpy(props->remote_password_, password.data(), password.size());
|
std::memcpy(props->remote_password_, password.data(), password.size());
|
||||||
props->remember_password_ = remember;
|
props->remember_password_ = remember;
|
||||||
props->rejoin_ = true;
|
props->rejoin_ = true;
|
||||||
password_validating_ = true;
|
|
||||||
need_to_rejoin_ = true;
|
need_to_rejoin_ = true;
|
||||||
});
|
});
|
||||||
main->on_request_screen_recording_permission([this] {
|
main->on_request_screen_recording_permission([this] {
|
||||||
@@ -1886,6 +1886,7 @@ void GuiApplication::Tick() {
|
|||||||
HandlePasswordChangeResult();
|
HandlePasswordChangeResult();
|
||||||
HandleCredentialRecovery();
|
HandleCredentialRecovery();
|
||||||
if (!peer_) {
|
if (!peer_) {
|
||||||
|
signal_connection_indicator_.Reset();
|
||||||
CreateConnectionPeer();
|
CreateConnectionPeer();
|
||||||
}
|
}
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
@@ -2477,7 +2478,10 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||||
ui_->main->set_password_change_pending(password_change_pending_);
|
ui_->main->set_password_change_pending(password_change_pending_);
|
||||||
}
|
}
|
||||||
ui_->main->set_signal_connected(signal_connected_);
|
const bool signal_online = signal_connected_.load();
|
||||||
|
ui_->main->set_signal_connected(signal_online);
|
||||||
|
ui_->main->set_signal_reconnecting(
|
||||||
|
signal_connection_indicator_.Update(signal_online));
|
||||||
ui_->main->set_signal_tls_error(signal_status_ ==
|
ui_->main->set_signal_tls_error(signal_status_ ==
|
||||||
SignalStatus::SignalTlsCertError);
|
SignalStatus::SignalTlsCertError);
|
||||||
ui_->main->set_update_available(update_available_);
|
ui_->main->set_update_available(update_available_);
|
||||||
@@ -2580,26 +2584,41 @@ void GuiApplication::SyncConnectionDialog() {
|
|||||||
}
|
}
|
||||||
ui_->connection_dialog_remote_id = props->remote_id_;
|
ui_->connection_dialog_remote_id = props->remote_id_;
|
||||||
const ConnectionStatus status = props->connection_status_.load();
|
const ConnectionStatus status = props->connection_status_.load();
|
||||||
const bool password_required = status == ConnectionStatus::IncorrectPassword;
|
const auto auth = props->connection_auth_.Get();
|
||||||
const bool pending = status == ConnectionStatus::Connecting ||
|
const bool password_required = auth.password_required ||
|
||||||
status == ConnectionStatus::Gathering;
|
(status == ConnectionStatus::IncorrectPassword && auth.validating);
|
||||||
|
const bool pending = !password_required &&
|
||||||
|
(status == ConnectionStatus::Connecting || status == ConnectionStatus::Gathering);
|
||||||
std::string text;
|
std::string text;
|
||||||
if (password_required) {
|
if (password_required) {
|
||||||
text = password_validating_
|
text = auth.validating
|
||||||
? localization::validate_password[localization_language_index_]
|
? localization::validate_password[localization_language_index_]
|
||||||
: password_validating_time_ <= 1
|
: auth.incorrect_password
|
||||||
? localization::input_password[localization_language_index_]
|
? localization::reinput_password[localization_language_index_]
|
||||||
: localization::reinput_password[localization_language_index_];
|
: localization::input_password[localization_language_index_];
|
||||||
|
} else if (auth.error == minirtc::JoinFailureKind::Throttled) {
|
||||||
|
text = localization::connection_auth_throttled[localization_language_index_];
|
||||||
|
} else if (auth.error == minirtc::JoinFailureKind::Busy) {
|
||||||
|
text = localization::connection_service_busy[localization_language_index_];
|
||||||
|
} else if (auth.error == minirtc::JoinFailureKind::NotAuthenticated) {
|
||||||
|
text = localization::connection_not_authenticated[localization_language_index_];
|
||||||
} else {
|
} else {
|
||||||
text = ConnectionStatusText(status, localization_language_index_);
|
text = ConnectionStatusText(status, localization_language_index_);
|
||||||
}
|
}
|
||||||
|
if (auth.retry_after > 0) {
|
||||||
|
char retry_text[256];
|
||||||
|
std::snprintf(retry_text, sizeof(retry_text),
|
||||||
|
localization::connection_retry_after[localization_language_index_].c_str(),
|
||||||
|
auth.retry_after);
|
||||||
|
text += "\n" + std::string(retry_text);
|
||||||
|
}
|
||||||
|
|
||||||
ui_->main->set_connection_dialog_open(true);
|
ui_->main->set_connection_dialog_open(true);
|
||||||
ui_->main->set_connection_status_text(UiText(text));
|
ui_->main->set_connection_status_text(UiText(text));
|
||||||
ui_->main->set_connection_pending(pending);
|
ui_->main->set_connection_pending(pending);
|
||||||
ui_->main->set_connection_password_required(password_required);
|
ui_->main->set_connection_password_required(password_required);
|
||||||
ui_->main->set_connection_validating(password_validating_);
|
ui_->main->set_connection_validating(auth.validating);
|
||||||
if (password_required && !password_validating_ &&
|
if (password_required && !auth.validating &&
|
||||||
!ui_->connection_password_initialized) {
|
!ui_->connection_password_initialized) {
|
||||||
ui_->main->set_connection_password(props->remote_password_);
|
ui_->main->set_connection_password(props->remote_password_);
|
||||||
ui_->main->set_connection_remember_password(props->remember_password_);
|
ui_->main->set_connection_remember_password(props->remember_password_);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include <remote_action.h>
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include "application/signal_connection_indicator.h"
|
||||||
#include "runtime/cursor_state_provider.h"
|
#include "runtime/cursor_state_provider.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ private:
|
|||||||
cursor_delivery_states_;
|
cursor_delivery_states_;
|
||||||
uint32_t cursor_state_sequence_ = 0;
|
uint32_t cursor_state_sequence_ = 0;
|
||||||
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||||
|
SignalConnectionIndicator signal_connection_indicator_;
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
bool use_xwayland_gui_ = false;
|
bool use_xwayland_gui_ = false;
|
||||||
bool use_x11_custom_titlebar_ = false;
|
bool use_x11_custom_titlebar_ = false;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef CROSSDESK_SIGNAL_CONNECTION_INDICATOR_H_
|
||||||
|
#define CROSSDESK_SIGNAL_CONNECTION_INDICATOR_H_
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// Presentation state only. The actual connected flag remains authoritative
|
||||||
|
// for actions and other UI consumers, including the announcement inbox.
|
||||||
|
class SignalConnectionIndicator {
|
||||||
|
public:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
bool Update(bool connected, Clock::time_point now = Clock::now()) {
|
||||||
|
if (connected) {
|
||||||
|
disconnected_at_.reset();
|
||||||
|
} else if (was_connected_) {
|
||||||
|
disconnected_at_ = now;
|
||||||
|
}
|
||||||
|
was_connected_ = connected;
|
||||||
|
return disconnected_at_ &&
|
||||||
|
now - *disconnected_at_ < std::chrono::seconds(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
was_connected_ = false;
|
||||||
|
disconnected_at_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool was_connected_ = false;
|
||||||
|
std::optional<Clock::time_point> disconnected_at_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -258,6 +258,9 @@ struct TranslationRow {
|
|||||||
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
||||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||||
u8"Нет подключения к серверу") \
|
u8"Нет подключения к серверу") \
|
||||||
|
X(signal_reconnect, u8"重新连接", "Reconnect", u8"Переподключиться") \
|
||||||
|
X(signal_reconnecting, u8"正在重新连接...", "Reconnecting...", \
|
||||||
|
u8"Переподключение...") \
|
||||||
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
|
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
|
||||||
"Certificate verification failed. Reinstall the self-hosted root " \
|
"Certificate verification failed. Reinstall the self-hosted root " \
|
||||||
"certificate.", \
|
"certificate.", \
|
||||||
@@ -272,6 +275,10 @@ struct TranslationRow {
|
|||||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||||
u8"Получение изображения...") \
|
u8"Получение изображения...") \
|
||||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||||
|
X(connection_auth_throttled, u8"验证尝试过于频繁,请稍后重试。", "Too many authentication attempts. Try again later.", u8"Слишком много попыток. Повторите позже.") \
|
||||||
|
X(connection_service_busy, u8"认证服务繁忙,请稍后重试。", "Authentication service busy. Try again later.", u8"Сервис проверки занят. Повторите позже.") \
|
||||||
|
X(connection_not_authenticated, u8"登录状态已失效,请重新连接。", "Signaling login expired. Please reconnect.", u8"Сеанс входа истёк. Подключитесь заново.") \
|
||||||
|
X(connection_retry_after, u8"请在 %d 秒后重试。", "Retry in %d seconds.", u8"Повторите через %d сек.") \
|
||||||
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
||||||
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
||||||
X(about, u8"关于", "About", u8"О программе") \
|
X(about, u8"关于", "About", u8"О программе") \
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#ifndef CROSSDESK_CONNECTION_AUTH_STATE_H_
|
||||||
|
#define CROSSDESK_CONNECTION_AUTH_STATE_H_
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "join_failure.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// One remote session's password prompt and server-requested retry delay.
|
||||||
|
// Signaling callbacks and the UI access a single synchronized snapshot.
|
||||||
|
class ConnectionAuthState {
|
||||||
|
public:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
struct Snapshot {
|
||||||
|
bool password_required = false;
|
||||||
|
bool validating = false;
|
||||||
|
bool incorrect_password = false;
|
||||||
|
minirtc::JoinFailureKind error = minirtc::JoinFailureKind::Unknown;
|
||||||
|
int retry_after = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool Begin(std::string_view password, Clock::time_point now = Clock::now()) {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
if (retry_at_ > now) return false;
|
||||||
|
state_ = {};
|
||||||
|
retry_at_ = {};
|
||||||
|
state_.password_required = password.empty();
|
||||||
|
state_.validating = !password.empty();
|
||||||
|
return state_.validating;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rejected(const nlohmann::json& reply,
|
||||||
|
Clock::time_point now = Clock::now()) {
|
||||||
|
const auto failure = minirtc::ParseJoinFailure(reply);
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
state_ = {};
|
||||||
|
state_.error = failure.kind;
|
||||||
|
state_.incorrect_password =
|
||||||
|
failure.kind == minirtc::JoinFailureKind::AuthenticationFailed;
|
||||||
|
state_.password_required =
|
||||||
|
state_.incorrect_password ||
|
||||||
|
failure.kind == minirtc::JoinFailureKind::PasswordRequired;
|
||||||
|
retry_at_ = now + std::chrono::seconds(failure.retry_after);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Complete() {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
state_.validating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot Get(Clock::time_point now = Clock::now()) const {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
auto snapshot = state_;
|
||||||
|
if (retry_at_ > now) {
|
||||||
|
snapshot.retry_after = static_cast<int>(
|
||||||
|
std::chrono::ceil<std::chrono::seconds>(retry_at_ - now).count());
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
Snapshot state_;
|
||||||
|
Clock::time_point retry_at_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -227,6 +227,14 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int GuiRuntime::ForceSignalReconnect() {
|
||||||
|
if (!peer_) {
|
||||||
|
LOG_WARN("Signal reconnect requested with no connection peer");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return ReconnectSignal(peer_);
|
||||||
|
}
|
||||||
|
|
||||||
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||||
if (!props || props->closing_.exchange(true)) return;
|
if (!props || props->closing_.exchange(true)) return;
|
||||||
if (props->peer_events_) props->peer_events_->Deactivate();
|
if (props->peer_events_) props->peer_events_->Deactivate();
|
||||||
@@ -479,6 +487,14 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
|||||||
|
|
||||||
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||||
bool remember_password, bool bypass_presence_check) {
|
bool remember_password, bool bypass_presence_check) {
|
||||||
|
// Do not recreate a failed peer or send another probe during a server cooldown.
|
||||||
|
if (const auto current = FindRemoteSession(remote_id);
|
||||||
|
current && current->connection_auth_.Get().retry_after > 0) {
|
||||||
|
focused_remote_id_ = remote_id;
|
||||||
|
show_connection_status_window_ = true;
|
||||||
|
current->rejoin_ = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (session_cleanup_tasks_.count(remote_id)) {
|
if (session_cleanup_tasks_.count(remote_id)) {
|
||||||
pending_reconnects_[remote_id] = {password ? password : "", remember_password};
|
pending_reconnects_[remote_id] = {password ? password : "", remember_password};
|
||||||
LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id);
|
LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id);
|
||||||
@@ -595,18 +611,23 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
|||||||
auto props = FindRemoteSession(remote_id);
|
auto props = FindRemoteSession(remote_id);
|
||||||
if (!props || props->closing_) return -1;
|
if (!props || props->closing_) return -1;
|
||||||
if (!props->connection_established_) {
|
if (!props->connection_established_) {
|
||||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
|
||||||
show_connection_status_window_ = true;
|
show_connection_status_window_ = true;
|
||||||
|
|
||||||
props->remember_password_ = remember_password;
|
props->remember_password_ = remember_password;
|
||||||
if (strcmp(password, "") != 0 &&
|
if (password && *password &&
|
||||||
strcmp(password, props->remote_password_) != 0) {
|
strcmp(password, props->remote_password_) != 0) {
|
||||||
strncpy(props->remote_password_, password,
|
strncpy(props->remote_password_, password,
|
||||||
sizeof(props->remote_password_) - 1);
|
sizeof(props->remote_password_) - 1);
|
||||||
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
|
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string remote_id_with_pwd = remote_id + "@" + password;
|
// Ask locally before Join; an empty-password round trip is no longer needed.
|
||||||
|
if (!props->connection_auth_.Begin(props->remote_password_)) {
|
||||||
|
props->rejoin_ = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||||
|
std::string remote_id_with_pwd = remote_id + "@" + props->remote_password_;
|
||||||
if (props->peer_) {
|
if (props->peer_) {
|
||||||
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
|
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
|
||||||
if (0 == ret) {
|
if (0 == ret) {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
bool remember_password, bool bypass_presence_check = false);
|
bool remember_password, bool bypass_presence_check = false);
|
||||||
int RequestSingleDevicePresence(const std::string& remote_id,
|
int RequestSingleDevicePresence(const std::string& remote_id,
|
||||||
const char* password, bool remember_password);
|
const char* password, bool remember_password);
|
||||||
|
int ForceSignalReconnect();
|
||||||
|
|
||||||
void HandleRecentConnections();
|
void HandleRecentConnections();
|
||||||
void HandleConnectionStatusChange();
|
void HandleConnectionStatusChange();
|
||||||
|
|||||||
@@ -79,7 +79,12 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string type = j["type"].get<std::string>();
|
std::string type = j["type"].get<std::string>();
|
||||||
if (type == "announcements_changed") {
|
if (type == "user_join_transmission" && j.contains("status") &&
|
||||||
|
j["status"] == "failed" && j.contains("transmission_id") &&
|
||||||
|
j["transmission_id"].is_string()) {
|
||||||
|
const auto props = runtime->FindRemoteSession(j["transmission_id"].get<std::string>());
|
||||||
|
if (props && !props->closing_) props->connection_auth_.Rejected(j);
|
||||||
|
} else if (type == "announcements_changed") {
|
||||||
runtime->announcements_.Refresh();
|
runtime->announcements_.Refresh();
|
||||||
} else if (type == "announcements") {
|
} else if (type == "announcements") {
|
||||||
runtime->announcements_.Receive(j);
|
runtime->announcements_.Receive(j);
|
||||||
@@ -265,6 +270,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
runtime->is_client_mode_ = true;
|
runtime->is_client_mode_ = true;
|
||||||
runtime->show_connection_status_window_ = true;
|
runtime->show_connection_status_window_ = true;
|
||||||
props->connection_status_.store(status);
|
props->connection_status_.store(status);
|
||||||
|
if (status != ConnectionStatus::Connecting && status != ConnectionStatus::Gathering)
|
||||||
|
props->connection_auth_.Complete();
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
@@ -385,11 +392,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ConnectionStatus::IncorrectPassword: {
|
|
||||||
runtime->password_validating_ = false;
|
|
||||||
runtime->password_validating_time_++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "minirtc.h"
|
#include "minirtc.h"
|
||||||
#include "rendering/video_latency.h"
|
#include "rendering/video_latency.h"
|
||||||
#include "runtime/net_traffic_stats_cache.h"
|
#include "runtime/net_traffic_stats_cache.h"
|
||||||
|
#include "runtime/connection_auth_state.h"
|
||||||
|
|
||||||
namespace crossdesk { class PeerEventHandler; }
|
namespace crossdesk { class PeerEventHandler; }
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ struct RemoteSession {
|
|||||||
bool p2p_mode_ = true;
|
bool p2p_mode_ = true;
|
||||||
bool remember_password_ = false;
|
bool remember_password_ = false;
|
||||||
char remote_password_[7] = "";
|
char remote_password_[7] = "";
|
||||||
|
ConnectionAuthState connection_auth_;
|
||||||
// Reused across frames by the decode callback thread when a CPU NV12
|
// Reused across frames by the decode callback thread when a CPU NV12
|
||||||
// native frame has to be materialized for a renderer that rejects it.
|
// native frame has to be materialized for a renderer that rejects it.
|
||||||
std::vector<unsigned char> native_cpu_fallback_;
|
std::vector<unsigned char> native_cpu_fallback_;
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export global UiStrings {
|
|||||||
in-out property <string> signal-connected: "Signal server connected";
|
in-out property <string> signal-connected: "Signal server connected";
|
||||||
in-out property <string> signal-disconnected: "Signal server disconnected";
|
in-out property <string> signal-disconnected: "Signal server disconnected";
|
||||||
in-out property <string> tls-error: "Signal server TLS certificate error";
|
in-out property <string> tls-error: "Signal server TLS certificate error";
|
||||||
|
in-out property <string> signal-reconnect: "Reconnect";
|
||||||
|
in-out property <string> signal-reconnecting: "Reconnecting...";
|
||||||
in-out property <string> update-available: "A new version is available";
|
in-out property <string> update-available: "A new version is available";
|
||||||
in-out property <string> new-version: "New version";
|
in-out property <string> new-version: "New version";
|
||||||
in-out property <string> check-for-updates: "Check for updates";
|
in-out property <string> check-for-updates: "Check for updates";
|
||||||
@@ -305,6 +307,7 @@ export component MainWindow inherits Window {
|
|||||||
in property <bool> password-visible: true;
|
in property <bool> password-visible: true;
|
||||||
in property <bool> password-change-pending: false;
|
in property <bool> password-change-pending: false;
|
||||||
in property <bool> signal-connected: false;
|
in property <bool> signal-connected: false;
|
||||||
|
in property <bool> signal-reconnecting: false;
|
||||||
in property <bool> signal-tls-error: false;
|
in property <bool> signal-tls-error: false;
|
||||||
in property <[RecentConnection]> recent-connections;
|
in property <[RecentConnection]> recent-connections;
|
||||||
in property <[AnnouncementItem]> announcements;
|
in property <[AnnouncementItem]> announcements;
|
||||||
@@ -402,6 +405,7 @@ export component MainWindow inherits Window {
|
|||||||
callback browse-save-path;
|
callback browse-save-path;
|
||||||
callback open-download;
|
callback open-download;
|
||||||
callback check-for-updates;
|
callback check-for-updates;
|
||||||
|
callback signal-reconnect-requested;
|
||||||
callback connection-cancel;
|
callback connection-cancel;
|
||||||
callback connection-acknowledge;
|
callback connection-acknowledge;
|
||||||
callback connection-submit-password(string, bool);
|
callback connection-submit-password(string, bool);
|
||||||
@@ -1209,7 +1213,7 @@ export component MainWindow inherits Window {
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: root.signal-tls-error ? #f59e0b : #ef4444;
|
background: root.signal-tls-error || root.signal-reconnecting ? #f59e0b : #ef4444;
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
x: 28px;
|
x: 28px;
|
||||||
@@ -1217,17 +1221,39 @@ export component MainWindow inherits Window {
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
text: root.signal-tls-error ? UiStrings.tls-error
|
text: root.signal-tls-error ? UiStrings.tls-error
|
||||||
: root.signal-connected ? UiStrings.signal-connected
|
: root.signal-connected ? UiStrings.signal-connected
|
||||||
|
: root.signal-reconnecting ? UiStrings.signal-reconnecting
|
||||||
: UiStrings.signal-disconnected;
|
: UiStrings.signal-disconnected;
|
||||||
color: #343941;
|
color: #343941;
|
||||||
font-size: ImGuiFontStyle.base;
|
font-size: ImGuiFontStyle.base;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
}
|
}
|
||||||
|
if !root.signal-connected && !root.signal-tls-error: Rectangle {
|
||||||
|
x: parent.width - self.width - 12px;
|
||||||
|
y: 3px;
|
||||||
|
height: 18px;
|
||||||
|
width: reconnect-label.preferred-width;
|
||||||
|
reconnect-label := Text {
|
||||||
|
text: UiStrings.signal-reconnect;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: ImGuiFontStyle.base;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
TouchArea {
|
||||||
|
mouse-cursor: pointer;
|
||||||
|
clicked => {
|
||||||
|
root.signal-reconnect-requested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match the ImGui recent-connection tooltip while keeping it outside the
|
// Match the ImGui recent-connection tooltip while keeping it outside the
|
||||||
// clipped ScrollView. It is deliberately limited to the thumbnail area.
|
// clipped ScrollView. It is deliberately limited to the thumbnail area.
|
||||||
if root.recent-tooltip-visible: Rectangle {
|
// Stay under the modal layer (z >= 39): a hover tooltip that outlives the
|
||||||
|
// pointer can otherwise cover a dialog button and hide it from the user.
|
||||||
|
if root.recent-tooltip-visible && !root.connection-dialog-open: Rectangle {
|
||||||
x: root.recent-tooltip-x;
|
x: root.recent-tooltip-x;
|
||||||
y: root.recent-tooltip-y;
|
y: root.recent-tooltip-y;
|
||||||
width: root.recent-tooltip-width;
|
width: root.recent-tooltip-width;
|
||||||
@@ -1236,7 +1262,7 @@ export component MainWindow inherits Window {
|
|||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: #ffffff38;
|
border-color: #ffffff38;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
z: 50;
|
z: 20;
|
||||||
|
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ inline int ApplyMainWindowStrings(
|
|||||||
strings.set_signal_disconnected(
|
strings.set_signal_disconnected(
|
||||||
Text(localization::signal_disconnected[language]));
|
Text(localization::signal_disconnected[language]));
|
||||||
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
|
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
|
||||||
|
strings.set_signal_reconnect(
|
||||||
|
Text(localization::signal_reconnect[language]));
|
||||||
|
strings.set_signal_reconnecting(
|
||||||
|
Text(localization::signal_reconnecting[language]));
|
||||||
strings.set_update_available(
|
strings.set_update_available(
|
||||||
Text(localization::new_version_available[language]));
|
Text(localization::new_version_available[language]));
|
||||||
strings.set_new_version(Text(localization::new_version[language]));
|
strings.set_new_version(Text(localization::new_version[language]));
|
||||||
|
|||||||
@@ -83,9 +83,7 @@ int main() {
|
|||||||
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
|
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
|
||||||
ok &= ExpectNotContains("minirtc.h", minirtc_api, "DeviceOffline");
|
ok &= ExpectNotContains("minirtc.h", minirtc_api, "DeviceOffline");
|
||||||
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
||||||
"\"Remote unavailable\"");
|
"on_connection_status_(ParseJoinFailure(j).status,");
|
||||||
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
|
||||||
"ConnectionStatus::RemoteUnavailable");
|
|
||||||
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
||||||
"IsCurrentPeerConnection");
|
"IsCurrentPeerConnection");
|
||||||
ok &= ExpectContains(
|
ok &= ExpectContains(
|
||||||
|
|||||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: a017ce97f2...0d55a84c58
Reference in New Issue
Block a user