Skip to content

Commit 37d6707

Browse files
committed
feat: 添加返回手势触觉反馈功能
thanks: HyperHelper
1 parent 64b10a8 commit 37d6707

8 files changed

Lines changed: 250 additions & 1 deletion

File tree

library/core/src/main/java/com/sevtinge/hyperceiler/hooker/home/HomeGestureSettings.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@
2727
import com.sevtinge.hyperceiler.core.R;
2828
import com.sevtinge.hyperceiler.dashboard.DashboardFragment;
2929

30+
import java.util.Objects;
31+
32+
import fan.preference.DropDownPreference;
3033
import fan.preference.SeekBarPreferenceCompat;
3134

3235
public class HomeGestureSettings extends DashboardFragment {
3336

3437
SwitchPreference mQuickBack;
3538
SwitchPreference mGestureEnable;
3639
SwitchPreference mDisableAllGesture;
40+
DropDownPreference mBackGestureHaptic;
3741
SeekBarPreferenceCompat mHighBackArea;
3842
SeekBarPreferenceCompat mWideBackArea;
3943
PreferenceCategory mGestureActions;
@@ -48,6 +52,7 @@ public void initPrefs() {
4852
mGestureEnable = findPreference("prefs_key_home_gesture_enable");
4953
mQuickBack = findPreference("prefs_key_home_navigation_quick_back");
5054
mDisableAllGesture = findPreference("prefs_key_home_navigation_disable_full_screen_back_gesture");
55+
mBackGestureHaptic = findPreference("prefs_key_home_gesture_back_haptic");
5156
mHighBackArea = findPreference("prefs_key_home_navigation_back_area_height");
5257
mWideBackArea = findPreference("prefs_key_home_navigation_back_area_width");
5358
mGestureActions = findPreference("prefs_key_home_gesture_actions");
@@ -60,11 +65,13 @@ public void initPrefs() {
6065
}
6166

6267
if (isPad()) {
68+
setFuncHint(mBackGestureHaptic, 1);
6369
setFuncHint(mDisableAllGesture, 1);
6470
} else if (isMoreHyperOSVersion(3f)) {
6571
mQuickBack.setVisible(false);
6672
mHighBackArea.setEnabled(mSwitch);
6773
mWideBackArea.setEnabled(mSwitch);
74+
updateBackGestureHapticSummary(getSharedPreferences().getString("prefs_key_home_gesture_back_haptic", "0"));
6875
}
6976

7077
mDisableAllGesture.setOnPreferenceChangeListener(
@@ -84,5 +91,25 @@ public void initPrefs() {
8491
return true;
8592
}
8693
);
94+
95+
if (mBackGestureHaptic != null && !isPad()) {
96+
mBackGestureHaptic.setOnPreferenceChangeListener((preference, newValue) -> {
97+
updateBackGestureHapticSummary((String) newValue);
98+
return true;
99+
});
100+
}
101+
}
102+
103+
private void updateBackGestureHapticSummary(String value) {
104+
if (mBackGestureHaptic == null) {
105+
return;
106+
}
107+
if (Objects.equals(value, "1")) {
108+
mBackGestureHaptic.setSummary(R.string.home_gesture_back_haptic_enhanced_tips);
109+
} else if (Objects.equals(value, "2")) {
110+
mBackGestureHaptic.setSummary("");
111+
} else {
112+
mBackGestureHaptic.setSummary(R.string.home_gesture_back_haptic_default_tips);
113+
}
87114
}
88115
}

library/core/src/main/res/values-zh-rCN/strings_app.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,11 @@
10651065
<string name="home_navigation_assist_right_slide">左上滑动</string>
10661066
<string name="home_navigation_assist_right_slide_desc">在任意界面从屏幕右下角向内滑动</string>
10671067
<string name="home_navigation_disable_full_screen_back_gesture">禁用全屏返回手势</string>
1068+
<string name="home_gesture_back_haptic">返回手势触觉反馈</string>
1069+
<string name="home_gesture_back_haptic_default_tips">在返回手势开始时震动</string>
1070+
<string name="home_gesture_back_haptic_enhanced">增强</string>
1071+
<string name="home_gesture_back_haptic_enhanced_tips">在返回手势开始和释放时震动</string>
1072+
<string name="home_gesture_back_haptic_disabled">禁用</string>
10681073
<string name="home_navigation_predictive_progress">修复预测性返回进度</string>
10691074
<string name="home_navigation_predictive_progress_desc">实验性功能,修复在 Xiaomi HyperOS 系统的部分场景下,预测性返回进度行程过短的问题</string>
10701075
<string name="home_navigation_back_area_height">返回手势区域高度</string>

library/core/src/main/res/values/arrays_app.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,18 @@
311311
<item>2</item>
312312
</string-array>
313313

314+
<string-array name="home_gesture_back_haptic">
315+
<item>@string/array_default</item>
316+
<item>@string/home_gesture_back_haptic_enhanced</item>
317+
<item>@string/home_gesture_back_haptic_disabled</item>
318+
</string-array>
319+
320+
<string-array name="home_gesture_back_haptic_value">
321+
<item>0</item>
322+
<item>1</item>
323+
<item>2</item>
324+
</string-array>
325+
314326
<string-array name="seek_points">
315327
<item>@string/array_default</item>
316328
<item>@string/seek_points_roll</item>

library/core/src/main/res/values/strings_app.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,11 @@
11141114
<string name="home_navigation_assist_right_slide_desc">Slide inward from the bottom right corner of the screen</string>
11151115
<string name="home_navigation_quick_back_more">Swipe across the screen starting from the edge to go back to previous app</string>
11161116
<string name="home_navigation_disable_full_screen_back_gesture">Disable full screen back gesture</string>
1117+
<string name="home_gesture_back_haptic">Back gesture haptic feedback</string>
1118+
<string name="home_gesture_back_haptic_default_tips">Perform haptic feedback on the start of swipes</string>
1119+
<string name="home_gesture_back_haptic_enhanced">Enhanced</string>
1120+
<string name="home_gesture_back_haptic_enhanced_tips">Perform haptic feedback on the start and release of swipes</string>
1121+
<string name="home_gesture_back_haptic_disabled">Disabled</string>
11171122
<string name="home_navigation_predictive_progress">Fix predictive back navigation progress</string>
11181123
<string name="home_navigation_predictive_progress_desc">Experimental feature: Fixes an issue where the predictive back navigation bar was too short in certain scenarios on the Xiaomi HyperOS system.</string>
11191124
<string name="home_navigation_back_area_height">Back gesture area height</string>

library/core/src/main/res/xml/home_gesture.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@
3939
android:summary="@string/home_navigation_predictive_progress_desc"
4040
android:title="@string/home_navigation_predictive_progress" />
4141

42+
<fan.preference.DropDownPreference
43+
android:defaultValue="0"
44+
android:key="prefs_key_home_gesture_back_haptic"
45+
android:title="@string/home_gesture_back_haptic"
46+
app:entries="@array/home_gesture_back_haptic"
47+
app:entryValues="@array/home_gesture_back_haptic_value" />
48+
4249
<SwitchPreference
4350
android:defaultValue="false"
4451
android:key="prefs_key_home_navigation_disable_full_screen_back_gesture"

library/libhook/src/main/java/com/sevtinge/hyperceiler/libhook/app/Home/os3/HomePad.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.sevtinge.hyperceiler.libhook.rules.home.folder.FolderAutoClose;
3737
import com.sevtinge.hyperceiler.libhook.rules.home.folder.FolderColumns;
3838
import com.sevtinge.hyperceiler.libhook.rules.home.folder.FolderVerticalSpacing;
39+
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.BackGestureHaptic;
3940
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.CornerSlide;
4041
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.DoubleTap;
4142
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.GestureLine;
@@ -108,6 +109,7 @@ public void onPackageLoaded() {
108109
|| PrefsBridge.getInt("home_gesture_line_double_click_action", 0) > 0;
109110

110111
// 手势
112+
initHook(new BackGestureHaptic(), PrefsBridge.getStringAsInt("home_gesture_back_haptic", 0) != 0);
111113
initHook(new PredictiveBackProgress(), PrefsBridge.getBoolean("home_navigation_predictive_progress"));
112114
initHook(new CornerSlide(), gesturesEnabled && hasCornerGestureAction);
113115
initHook(new DoubleTap(), gesturesEnabled && PrefsBridge.getInt("home_gesture_double_tap_action", 0) > 0);

library/libhook/src/main/java/com/sevtinge/hyperceiler/libhook/app/Home/os3/HomePhone.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.sevtinge.hyperceiler.libhook.rules.home.folder.FolderAutoClose;
3737
import com.sevtinge.hyperceiler.libhook.rules.home.folder.FolderColumns;
3838
import com.sevtinge.hyperceiler.libhook.rules.home.folder.FolderVerticalSpacing;
39+
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.BackGestureHaptic;
3940
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.CornerSlide;
4041
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.DoubleTap;
4142
import com.sevtinge.hyperceiler.libhook.rules.home.gesture.GestureLine;
@@ -97,7 +98,6 @@
9798
@HookBase(targetPackage = "com.miui.home", deviceType = 2, minOSVersion = 3.0F)
9899
public class HomePhone extends BaseLoad {
99100

100-
101101
@Override
102102
public void onPackageLoaded() {
103103
boolean gesturesEnabled = PrefsBridge.getBoolean("home_gesture_enable");
@@ -112,6 +112,7 @@ public void onPackageLoaded() {
112112

113113
// 手势
114114
initHook(new DisableFullScreenBackGesture(), PrefsBridge.getBoolean("home_navigation_disable_full_screen_back_gesture"));
115+
initHook(new BackGestureHaptic(), PrefsBridge.getStringAsInt("home_gesture_back_haptic", 0) != 0);
115116
initHook(new PredictiveBackProgress(), PrefsBridge.getBoolean("home_navigation_predictive_progress"));
116117
initHook(new CornerSlide(), gesturesEnabled && hasCornerGestureAction);
117118
initHook(new DoubleTap(), gesturesEnabled && PrefsBridge.getInt("home_gesture_double_tap_action", 0) > 0);
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* This file is part of HyperCeiler.
3+
*
4+
* HyperCeiler is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*
17+
* Copyright (C) 2023-2026 HyperCeiler Contributions
18+
*/
19+
package com.sevtinge.hyperceiler.libhook.rules.home.gesture;
20+
21+
import com.sevtinge.hyperceiler.common.log.XposedLog;
22+
import com.sevtinge.hyperceiler.common.utils.PrefsBridge;
23+
import com.sevtinge.hyperceiler.libhook.base.BaseHook;
24+
import com.sevtinge.hyperceiler.libhook.callback.IMethodHook;
25+
26+
import java.lang.reflect.Method;
27+
import java.util.Set;
28+
29+
import io.github.kyuubiran.ezxhelper.xposed.common.HookParam;
30+
31+
// https://github.com/HowieHChen/XiaomiHelper/blob/fb2462aa819eed36b6b8875c85f9cb0887eb1ccd/app/src/main/kotlin/dev/lackluster/mihelper/hook/rules/miuihome/gesture/BackGestureHaptic.kt
32+
public class BackGestureHaptic extends BaseHook {
33+
34+
private static final String TIME_OUT_BLOCKER_KEY = "BLOCKER_ID_FOR_HAPTIC_GESTURE_BACK";
35+
36+
@Override
37+
public void init() {
38+
int mode = PrefsBridge.getStringAsInt("home_gesture_back_haptic", 0);
39+
if (mode == 1) {
40+
initEnhancedMode();
41+
} else if (mode == 2) {
42+
initDisabledMode();
43+
}
44+
}
45+
46+
private void initEnhancedMode() {
47+
Class<?> hapticFeedbackCompatV2 = findClassIfExists("com.miui.home.common.hapticfeedback.HapticFeedbackCompatV2");
48+
if (hapticFeedbackCompatV2 == null) {
49+
return;
50+
}
51+
52+
hookNoArgMethods(hapticFeedbackCompatV2, "performGestureReadyBack", new IMethodHook() {
53+
@Override
54+
public void after(HookParam param) {
55+
startTimeOutBlocker();
56+
}
57+
});
58+
59+
hookNoArgMethods(hapticFeedbackCompatV2, "performGestureBackHandUp", new IMethodHook() {
60+
@Override
61+
public void before(HookParam param) {
62+
if (isTimeOutBlocked()) {
63+
param.setResult(null);
64+
}
65+
}
66+
});
67+
68+
hookLambdaMethods(hapticFeedbackCompatV2, "performGestureReadyBack", new IMethodHook() {
69+
@Override
70+
public void before(HookParam param) {
71+
performGestureHaptic(param.getThisObject(), 0);
72+
param.setResult(null);
73+
}
74+
});
75+
76+
hookLambdaMethods(hapticFeedbackCompatV2, "performGestureBackHandUp", new IMethodHook() {
77+
@Override
78+
public void before(HookParam param) {
79+
performGestureHaptic(param.getThisObject(), 1);
80+
param.setResult(null);
81+
}
82+
});
83+
84+
Class<?> gestureStubViewClass = findClassIfExists("com.miui.home.recents.GestureStubView");
85+
if (gestureStubViewClass != null) {
86+
for (Method method : gestureStubViewClass.getDeclaredMethods()) {
87+
if (!"injectBackKeyEvent".equals(method.getName())) {
88+
continue;
89+
}
90+
Class<?>[] parameterTypes = method.getParameterTypes();
91+
if (parameterTypes.length != 1 || parameterTypes[0] != boolean.class) {
92+
continue;
93+
}
94+
method.setAccessible(true);
95+
hookMethod(method, new IMethodHook() {
96+
@Override
97+
public void before(HookParam param) {
98+
param.getArgs()[0] = true;
99+
}
100+
});
101+
}
102+
}
103+
}
104+
105+
private void initDisabledMode() {
106+
Set<String> classNames = Set.of(
107+
"com.miui.home.common.hapticfeedback.HapticFeedbackCompatLinear",
108+
"com.miui.home.common.hapticfeedback.HapticFeedbackCompatNormal",
109+
"com.miui.home.common.hapticfeedback.HapticFeedbackCompatV2"
110+
);
111+
112+
for (String className : classNames) {
113+
Class<?> hapticClass = findClassIfExists(className);
114+
if (hapticClass == null) {
115+
continue;
116+
}
117+
IMethodHook disabledHook = new IMethodHook() {
118+
@Override
119+
public void before(HookParam param) {
120+
param.setResult(null);
121+
}
122+
};
123+
hookNoArgMethods(hapticClass, "performGestureReadyBack", disabledHook);
124+
hookNoArgMethods(hapticClass, "performGestureBackHandUp", disabledHook);
125+
}
126+
}
127+
128+
private void startTimeOutBlocker() {
129+
try {
130+
Class<?> backgroundThreadClass = findClassIfExists("com.miui.home.common.multithread.BackgroundThread");
131+
Class<?> timeOutBlockerClass = findClassIfExists("com.miui.home.common.utils.TimeOutBlocker");
132+
if (backgroundThreadClass == null || timeOutBlockerClass == null) {
133+
return;
134+
}
135+
Object handler = callStaticMethod(backgroundThreadClass, "getHandler");
136+
if (handler != null) {
137+
callStaticMethod(timeOutBlockerClass, "startCountDown", handler, 140L, TIME_OUT_BLOCKER_KEY);
138+
}
139+
} catch (Throwable throwable) {
140+
XposedLog.w(TAG, getPackageName(), "start back gesture haptic blocker failed", throwable);
141+
}
142+
}
143+
144+
private boolean isTimeOutBlocked() {
145+
try {
146+
Class<?> timeOutBlockerClass = findClassIfExists("com.miui.home.common.utils.TimeOutBlocker");
147+
if (timeOutBlockerClass == null) {
148+
return false;
149+
}
150+
Object blocked = callStaticMethod(timeOutBlockerClass, "isBlocked", TIME_OUT_BLOCKER_KEY);
151+
return blocked instanceof Boolean && (Boolean) blocked;
152+
} catch (Throwable throwable) {
153+
XposedLog.w(TAG, getPackageName(), "check back gesture haptic blocker failed", throwable);
154+
return false;
155+
}
156+
}
157+
158+
private void performGestureHaptic(Object target, int effectId) {
159+
Object hapticHelper = getObjectField(target, "mHapticHelper");
160+
if (hapticHelper == null) {
161+
return;
162+
}
163+
try {
164+
callMethod(hapticHelper, "performExtHapticFeedback", effectId);
165+
} catch (Throwable throwable) {
166+
XposedLog.w(TAG, getPackageName(), "perform back gesture haptic feedback failed", throwable);
167+
}
168+
}
169+
170+
private void hookNoArgMethods(Class<?> targetClass, String methodName, IMethodHook callback) {
171+
for (Method method : targetClass.getDeclaredMethods()) {
172+
if (!methodName.equals(method.getName()) || method.getParameterCount() != 0) {
173+
continue;
174+
}
175+
method.setAccessible(true);
176+
hookMethod(method, callback);
177+
}
178+
}
179+
180+
private void hookLambdaMethods(Class<?> targetClass, String methodTag, IMethodHook callback) {
181+
for (Method method : targetClass.getDeclaredMethods()) {
182+
String methodName = method.getName();
183+
if (!methodName.startsWith("lambda") || !methodName.contains(methodTag)) {
184+
continue;
185+
}
186+
method.setAccessible(true);
187+
hookMethod(method, callback);
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)