Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 15 additions & 21 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
## 0.9.0 更新日志
## 0.9.1 更新日志

1. 新增「丰富侧滑震动反馈」实验功能
侧滑进入 Ready 状态时会补充一次更自然的系统原生震动反馈,让手势阶段感更清晰
1. 修复 Launcher `8.01.02.6230` 下 Native Hook 目标识别失败的问题。
新版 Launcher 中旧版 exact fingerprint 会同时命中两个函数,0.9.1 不再因为多候选直接拒绝安装,而是使用 `BackGestureUtils::convert_offset` 的完整行为链逐个验证候选,仅在能够唯一证明真实 `on_swipe_process` 时安装 Hook

2. 优化返回手势震动逻辑
减少短时间内重复震动的问题,同时尽量保留系统原有的侧滑、停顿和松手反馈
2. 保持 fail-closed,不为 6230 写死 RVA。
本次兼容没有加入版本白名单、固定地址、优先新版 pattern 或“取第一个候选”等不安全 fallback;如果多个候选仍无法唯一验证,模块会继续保持原厂行为

3. 重做侧滑动画进度同步
自定义触发距离后,返回动画会跟随新的阈值平滑延展,不再出现动画提前到位、停住或到达阈值时突然跳动的问题
3. 收紧 semantic resolver 的主 Hook 权限。
反编译确认当前 MotionEvent semantic graph 在 6230 会定位到外层 `MotionEvent*` 事件处理函数,与主 Hook 的 `w1 / w2 / s0` ABI 不一致。因此 semantic 目前只用于诊断和交叉验证,不再在没有可信 exact 目标时单独接管主 Hook,降低 Launcher 崩溃风险

4. 优化高阈值下的完整动画距离
修复阈值设置越大,Ready 之后还需要继续滑很长距离才能完成动画的问题。现在 Ready 后的动画距离更接近系统原厂手感
4. 改进 Native diagnostics。
多候选时会分别记录 candidate RVA、fingerprint、`convert_offset` RVA、corroborated callsites 和 qualification 结果;失败日志也会区分 exact / semantic candidate 数量及具体 failure reason,方便后续适配 Launcher 更新

5. 提升 Launcher 8.x 兼容性与稳定性
加强 Hook 识别、异常保护和自动恢复机制,系统桌面更新后如果无法安全识别目标,会优先保持原厂行为,降低异常风险。

6. 新增「启动动画期间立即返回 · Beta」
从桌面打开 App 后,无需等待启动动画结束即可侧滑返回,保持更连贯的返回操作体验。

7. 完善免 Root 控制与诊断
优化 App、SystemUI 与 Launcher 之间的配置同步和状态检测,诊断信息更加完整,排查问题更方便。
5. 保持已有 Launcher 兼容路径不变。
对原本只有一个已验证 exact fingerprint 的 5459 / 6174 / 6179 等版本,仍沿用原来的 exact-authoritative 路径,不额外强制新的行为验证条件,尽量减少回归面。

### 本次重点

- 自定义侧滑阈值的动画体验大幅改善
- 高阈值下手势距离更合理
- 震动反馈更自然
- Launcher 8.x 兼容性与稳定性进一步提升
- 新增 Launcher 8.01.02.6230 安全兼容
- 多 exact 候选可通过行为证据自动消歧
- semantic-only 错 ABI 风险收口
- 诊断信息更完整
6 changes: 3 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ android {
applicationId = "io.github.pzhown.hyperos4swipegate"
minSdk = 33
targetSdk = 37
// 0.9.0: stable release with synchronized Back progress, native haptics and break-open support.
versionCode = 64
versionName = "0.9.0"
// 0.9.1: compatibility release with safer Launcher 6230 target disambiguation.
versionCode = 65
versionName = "0.9.1"

ndk {
abiFilters += "arm64-v8a"
Expand Down
21 changes: 15 additions & 6 deletions docs/SEMANTIC_RESOLVER.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Semantic Resolver

本文记录 SwipeGate 当前用于定位 HyperOS 4 Launcher 8.x `GestureInputBackHelper::on_swipe_process` 的语义解析策略。旧文档中关于“仅 dev 验证、main 仍停留在 0.6.0”的描述已经过时;当前架构已长期使用 semantic resolver,并继续保留 exact fingerprint 作为保守 fallback
本文记录 SwipeGate 当前用于定位 HyperOS 4 Launcher 8.x `GestureInputBackHelper::on_swipe_process` 的语义解析策略。旧文档中关于“仅 dev 验证、main 仍停留在 0.6.0”的描述已经过时;当前主 Hook 以 exact fingerprint 为兼容性契约;当 fingerprint 出现多个候选时,使用 `BackGestureUtils::convert_offset` 行为链消歧。MotionEvent semantic resolver 目前仅用于诊断与交叉验证,不再在没有 exact 目标时单独接管主 Hook

## 目标

Expand Down Expand Up @@ -62,11 +62,11 @@ gesture-frame-v2

决策原则:

1. semantic 唯一,exact 无候选:接受 semantic
2. semantic 与 exact 指向同一唯一地址:接受
3. semantic 与 exact 冲突且无法证明哪一侧可信:fail closed
4. semantic 不稳定,但已验证 exact fingerprint 唯一且完整:允许 exact-authoritative fallback
5. 0 candidate 或 multiple candidates:fail closed
1. exact 唯一:保持 exact-authoritative;semantic 可 corroborate,但不能否决已验证 exact
2. exact 多候选:逐个验证 `convert_offset` 行为链;仅一个通过时接受 `exact-behavior-disambiguated`
3. exact 多候选且 0 个或多个行为候选通过:fail closed
4. exact 无候选:MotionEvent semantic 仅报告候选,不安装主 Hook;当前 ABI 未经证明时 fail closed
5. semantic 与 exact 指向同一唯一地址:记录 `semantic-corroborated`;冲突时记录但仍由唯一 exact authoritative

6179 实机当前主 Hook 日志可出现:

Expand All @@ -77,6 +77,15 @@ pattern=gesture-frame-v2

这表示 semantic 图在该编译版本上与 exact 结果不完全一致,但 `gesture-frame-v2` 仍是唯一已验证目标,因此采用 exact authoritative 路径;不是使用硬编码 RVA。

6230 APK 静态验证出现两个 exact fingerprint:`gesture-frame-v1` 与 `gesture-frame-v2`。其中只有 v2 候选通过 `BackGestureUtils::convert_offset` 行为链验证,因此生产 resolver 应输出:

```text
detail=exact-behavior-disambiguated
pattern=gesture-frame-v2
```

这是行为证据消歧,不是 6230 版本白名单或固定 RVA fallback。

## ABI-transparent wrapper

主 Hook wrapper 使用 AArch64 汇编保存:
Expand Down
157 changes: 124 additions & 33 deletions native/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ constexpr uint32_t kMovW8Float110 = 0x52a85b88u;
constexpr size_t kHookProbeSize = 16;
constexpr size_t kMaxExecutableRanges = 12;
constexpr size_t kMaxLoadRanges = 16;
constexpr size_t kMaxExactCandidates = 8;

constexpr uintptr_t kReferenceOnSwipeProcessOffset = 0x816fc4;

Expand Down Expand Up @@ -495,14 +496,38 @@ struct PatternMatch {
const char *detail = nullptr;
};

struct ExactCandidateSet {
std::array<PatternMatch, kMaxExactCandidates> matches{};
size_t count = 0;
bool overflow = false;
};

struct ExactCandidateEvidence {
PatternMatch match{};
uintptr_t progressTarget = 0;
size_t corroboratedCallsites = 0;
bool qualified = false;
};

struct TargetResolution {
PatternMatch match{};
size_t uniqueCandidates = 0;
size_t exactCandidates = 0;
size_t exactQualifiedCandidates = 0;
bool exactOverflow = false;
size_t semanticCandidates = 0;
uintptr_t semanticTarget = 0;
swipe_semantic::FrameShape semanticShape = swipe_semantic::FrameShape::Unknown;
std::array<ExactCandidateEvidence, kMaxExactCandidates> exactEvidence{};
const char *failureDetail = nullptr;
};

TargetResolution resolveExactOnSwipeProcessTarget(const LibraryInfo &library) {
std::vector<PatternMatch> matches;
matches.reserve(2);
uintptr_t resolveBackProgressConvertOffsetTarget(
const LibraryInfo &library, uintptr_t onSwipeProcessTarget,
size_t *corroboratedCallsites);

ExactCandidateSet resolveExactOnSwipeProcessTarget(const LibraryInfo &library) {
ExactCandidateSet result;

for (const PatternSpec &pattern : kOnSwipeProcessPatterns) {
for (size_t rangeIndex = 0; rangeIndex < library.executableRangeCount; ++rangeIndex) {
Expand All @@ -513,46 +538,90 @@ TargetResolution resolveExactOnSwipeProcessTarget(const LibraryInfo &library) {
const uintptr_t last = range.start + range.size - pattern.size;
for (uintptr_t cursor = alignedStart; cursor <= last; cursor += 4U) {
if (!patternMatchesAt(cursor, pattern)) continue;
const auto duplicate = std::find_if(matches.begin(), matches.end(),
[cursor](const PatternMatch &item) { return item.address == cursor; });
if (duplicate == matches.end()) {
matches.push_back({cursor, &pattern, nullptr});
if (matches.size() > 1) return {{}, matches.size()};
bool duplicate = false;
for (size_t i = 0; i < result.count; ++i) {
if (result.matches[i].address == cursor) {
duplicate = true;
break;
}
}
if (duplicate) continue;
if (result.count >= result.matches.size()) {
result.overflow = true;
return result;
}
result.matches[result.count++] = {cursor, &pattern, nullptr};
}
}
}

if (matches.size() == 1) return {matches.front(), 1};
return {{}, matches.size()};
return result;
}

TargetResolution resolveOnSwipeProcessTarget(const LibraryInfo &library) {
const swipe_semantic::Resolution semantic = swipe_semantic::Resolve(library.base);
const TargetResolution exact = resolveExactOnSwipeProcessTarget(library);
const ExactCandidateSet exact = resolveExactOnSwipeProcessTarget(library);

TargetResolution result;
result.exactCandidates = exact.count;
result.exactOverflow = exact.overflow;
result.semanticCandidates = semantic.candidate_count;
result.semanticTarget = semantic.target;
result.semanticShape = semantic.shape;
for (size_t i = 0; i < exact.count; ++i) {
result.exactEvidence[i].match = exact.matches[i];
}

if (exact.overflow) {
result.failureDetail = "exact-candidate-overflow";
return result;
}

// A unique exact profile is an already validated compatibility contract.
// Semantic resolution may corroborate it, but cannot disable or replace it.
// This keeps 5459/6174 safe while the semantic matcher evolves on real devices.
if (exact.uniqueCandidates == 1 && exact.match.address != 0 && exact.match.pattern != nullptr) {
if (exact.count == 1 && exact.matches[0].address != 0
&& exact.matches[0].pattern != nullptr) {
const PatternMatch &match = exact.matches[0];
const char *detail = "exact-authoritative";
if (semantic.candidate_count == 1 && semantic.target != 0) {
const bool agrees = semantic.target == exact.match.address;
return {{exact.match.address, exact.match.pattern,
agrees ? "semantic-corroborated" : "semantic-conflict-exact-authoritative"}, 1};
detail = semantic.target == match.address
? "semantic-corroborated"
: "semantic-conflict-exact-authoritative";
}
return {{exact.match.address, exact.match.pattern, "exact-authoritative"}, 1};
result.match = {match.address, match.pattern, detail};
result.uniqueCandidates = 1;
return result;
}

// Multiple exact hits are unsafe even if semantic happens to choose one.
if (exact.uniqueCandidates > 1) return {{}, exact.uniqueCandidates};

// Unknown launcher build: semantic resolution is allowed to take over only
// after proving exactly one candidate. Otherwise fail closed.
if (semantic.candidate_count == 1 && semantic.target != 0) {
return {{semantic.target, &kSemanticOnSwipeProcessPattern,
swipe_semantic::FrameShapeName(semantic.shape)}, 1};
if (exact.count > 1) {
size_t qualified = 0;
PatternMatch selected{};
for (size_t i = 0; i < exact.count; ++i) {
size_t corroboratedCallsites = 0;
const uintptr_t progressTarget = resolveBackProgressConvertOffsetTarget(
library, exact.matches[i].address, &corroboratedCallsites);
ExactCandidateEvidence &evidence = result.exactEvidence[i];
evidence.progressTarget = progressTarget;
evidence.corroboratedCallsites = corroboratedCallsites;
evidence.qualified = progressTarget != 0;
if (!evidence.qualified) continue;
selected = exact.matches[i];
++qualified;
}
result.exactQualifiedCandidates = qualified;
if (qualified == 1 && selected.address != 0 && selected.pattern != nullptr) {
result.match = {selected.address, selected.pattern,
"exact-behavior-disambiguated"};
result.uniqueCandidates = 1;
return result;
}
result.failureDetail = qualified == 0
? "exact-multiple-no-qualified-target"
: "exact-multiple-behavior-still-ambiguous";
return result;
}
return {{}, semantic.candidate_count};

result.failureDetail = semantic.candidate_count == 1 && semantic.target != 0
? "semantic-only-primary-abi-unverified"
: "no-validated-exact-target";
return result;
}

int parseDensityDpi(const char *text) {
Expand Down Expand Up @@ -1519,13 +1588,35 @@ bool ensureHookLocked(const LibraryInfo &library, const char *source) {
gLastPatternScanMs.store(now, std::memory_order_relaxed);

const TargetResolution resolution = resolveOnSwipeProcessTarget(library);
if (resolution.exactCandidates > 1 || resolution.exactOverflow) {
for (size_t i = 0; i < resolution.exactCandidates; ++i) {
const ExactCandidateEvidence &evidence = resolution.exactEvidence[i];
const uintptr_t candidateRva = evidence.match.address >= library.base
? evidence.match.address - library.base : 0u;
const uintptr_t progressRva = evidence.progressTarget >= library.base
? evidence.progressTarget - library.base : 0u;
logLine(ANDROID_LOG_INFO,
"HOOK_SCAN exact candidate source=%s index=%zu rva=0x%zx pattern=%s progressQualified=%d convertOffsetRva=0x%zx corroboratedCalls=%zu",
source, i, static_cast<size_t>(candidateRva),
evidence.match.pattern == nullptr ? "<none>" : evidence.match.pattern->name,
evidence.qualified ? 1 : 0, static_cast<size_t>(progressRva),
evidence.corroboratedCallsites);
}
}
if (resolution.uniqueCandidates != 1 || resolution.match.address == 0 || resolution.match.pattern == nullptr) {
gHookInstalled.store(false, std::memory_order_release);
gHookHealthState.store(3, std::memory_order_release);
const uintptr_t semanticRva = resolution.semanticTarget >= library.base
? resolution.semanticTarget - library.base : 0u;
logLine(ANDROID_LOG_ERROR,
"HOOK_SCAN install refused source=%s candidates=%zu base=%p execRanges=%zu referenceOffset=0x%zx; semantic/exact resolvers found no unique validated target",
source, resolution.uniqueCandidates, reinterpret_cast<void *>(library.base),
library.executableRangeCount, static_cast<size_t>(kReferenceOnSwipeProcessOffset));
"HOOK_SCAN install refused source=%s candidates=%zu exactCandidates=%zu exactQualified=%zu exactOverflow=%d semanticCandidates=%zu semanticRva=0x%zx semanticShape=%s failure=%s base=%p execRanges=%zu referenceOffset=0x%zx",
source, resolution.uniqueCandidates, resolution.exactCandidates,
resolution.exactQualifiedCandidates, resolution.exactOverflow ? 1 : 0,
resolution.semanticCandidates, static_cast<size_t>(semanticRva),
swipe_semantic::FrameShapeName(resolution.semanticShape),
resolution.failureDetail == nullptr ? "unknown" : resolution.failureDetail,
reinterpret_cast<void *>(library.base), library.executableRangeCount,
static_cast<size_t>(kReferenceOnSwipeProcessOffset));
return false;
}

Expand Down Expand Up @@ -1679,7 +1770,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
gUnhookFunction = entries->unhook_func;
if (launcherProcess) swipegate_back_break_enable(entries->hook_func, entries->unhook_func);
logLine(ANDROID_LOG_INFO,
"DP_GATE native_init accepted api=%u exe=%s process=%s launcherCmdline=%d hook_func=%p unhook_func=%p watchdog=%lldms resolver=exact-profile-first+semantic-unknown-build abi=transparent-s0 repair=unhook+rehook",
"DP_GATE native_init accepted api=%u exe=%s process=%s launcherCmdline=%d hook_func=%p unhook_func=%p watchdog=%lldms resolver=exact-profile+back-progress-disambiguation semantic=diagnostic-only abi=transparent-s0 repair=unhook+rehook",
entries->version, executable.c_str(), processName.c_str(), launcherProcess ? 1 : 0,
reinterpret_cast<void *>(entries->hook_func), reinterpret_cast<void *>(entries->unhook_func),
static_cast<long long>(kHookHealthIntervalMs));
Expand Down
Loading