Skip to content

Commit a361a04

Browse files
committed
Merge branch 'master' into breaded
# Conflicts: # README.md # app/nonRoot/release/baselineProfiles/0/app-nonRoot-release.dm # app/nonRoot/release/baselineProfiles/1/app-nonRoot-release.dm
2 parents 1faf9e5 + b2e0c67 commit a361a04

39 files changed

Lines changed: 3239 additions & 842 deletions

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
# Moonlight Android
2+
3+
此项目实现对[Moonlight安卓端 威力增强版](https://github.com/qiin2333/moonlight-vplus)的功能改进<br><br>
4+
5+
6+
# 特性
7+
8+
* **修复了开启无障碍服务后无法使用音量键的问题**
9+
10+
* **修复了Ctrl+Shift+Alt+C在本地鼠标模式下无法正确切换模式的问题**
11+
12+
* **自动获取无障碍权限**
13+
> *需要通过ADB授权android.permission.WRITE_SECURE_SETTINGS*
14+
15+
* **自动切换远程电脑的光标显示**
16+
> *在本地鼠标模式下,自动根据状态发送电脑光标显示开关请求*
17+
18+
* **安卓本地鼠标特殊滚轮的识别**
19+
> *在使用安卓本地鼠标光标时,部分设备无法正确获取滚轮事件,如华为安卓平板,滚动滚动时会产生触屏效果造成大范围滑动*
20+
21+
* **适用于部分平板键盘的按键映射**
22+
> * Home → **Esc**
23+
> * Esc + [1-0, -, =]**F1 - F12**
24+
> * Esc + Q → **呼出串流菜单**
25+
> * 安卓截图键 → **Windows 截图 (Win+Shift+S)**
26+
> * 安卓媒体键 (上一首/播放暂停/下一首) → **F5/F10/F11**
27+
28+
* **支持鼠标中键**
29+
> *部分设备将鼠标中键识别为返回键,此功能开启可以区分触摸返回和鼠标中键造成的返回*
30+
31+
<br><br>
32+
133
<div align="center">
234
<img src="./app/src/main/res/drawable/vplus.webp" width="100" alt="Moonlight V+ Logo">
335

app/build.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ android {
147147
// is to please change the applicationId before you publish.
148148
//
149149
// TL;DR: Leave the following line alone!
150-
applicationIdSuffix ".qiin"
150+
applicationIdSuffix ".breaded"
151151
resValue "string", "app_label", "Moonlight"
152152
resValue "string", "app_label_root", "Moonlight (Root)"
153153

@@ -190,6 +190,8 @@ dependencies {
190190
implementation 'androidx.tvprovider:tvprovider:1.1.0'
191191

192192
// Firebase Analytics
193-
implementation platform('com.google.firebase:firebase-bom:32.7.0')
193+
// BOM 版本说明: https://firebase.google.com/support/release-notes/android
194+
// 注意: Firebase 33.0.0+ 需要 minSdk 23,我们使用 32.8.1 保持 API 22 兼容
195+
implementation platform('com.google.firebase:firebase-bom:32.8.1')
194196
implementation 'com.google.firebase:firebase-analytics'
195197
}
1.45 KB
Binary file not shown.
1.44 KB
Binary file not shown.

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
android:name=".preferences.StreamSettings"
149149
android:resizeableActivity="true"
150150
android:enableOnBackInvokedCallback="true"
151-
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
151+
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|smallestScreenSize|layoutDirection"
152152
android:label="Streaming Settings">
153153
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
154154
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />

app/src/main/java/com/limelight/Game.java

Lines changed: 153 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@
9292
import android.widget.TextView;
9393
import android.widget.Toast;
9494

95-
import androidx.annotation.RequiresApi;
96-
9795
import java.io.ByteArrayInputStream;
9896
import java.lang.reflect.InvocationTargetException;
9997
import java.lang.reflect.Method;
@@ -175,7 +173,10 @@ public interface PerformanceInfoDisplay {
175173
private boolean surfaceCreated = false;
176174
private boolean attemptedConnection = false;
177175
private AnalyticsManager analyticsManager;
178-
private long streamStartTime;
176+
private long streamStartTime; // 串流开始的时间戳
177+
private long accumulatedStreamTime; // 累计的有效串流时间(排除后台暂停)
178+
private long lastActiveTime; // 上次活跃的时间戳(用于计算暂停时间)
179+
private boolean isStreamingActive; // 串流是否处于活跃状态
179180
private int suppressPipRefCount = 0;
180181
private String pcName;
181182
private String appName;
@@ -363,6 +364,20 @@ private void stopAndUnbindUsbDriverService() {
363364
private boolean detectScrolling = false;
364365
private boolean detectMouseMiddle = false;
365366
private boolean detectMouseMiddleDown = false;
367+
private static final String CURSOR_PREFS = "CursorPrefs";
368+
private static final String KEY_CURSOR_RESTORE = "cursor_switch_state";
369+
370+
private void saveCursorState(int state) {
371+
getSharedPreferences(CURSOR_PREFS, MODE_PRIVATE)
372+
.edit()
373+
.putInt(KEY_CURSOR_RESTORE, state)
374+
.commit(); // 使用 commit 确保即使进程随后被杀也能保存
375+
}
376+
377+
private int getCursorState() {
378+
return getSharedPreferences(CURSOR_PREFS, MODE_PRIVATE)
379+
.getInt(KEY_CURSOR_RESTORE, 0);
380+
}
366381

367382
@Override
368383
protected void onCreate(Bundle savedInstanceState) {
@@ -1103,6 +1118,9 @@ public void onStreamViewReady(StreamView streamView) {
11031118
protected void onResume() {
11041119
super.onResume();
11051120

1121+
// 回到游戏时,重置标志位,允许下次暂停时再次恢复
1122+
isCursorRestored = false;
1123+
11061124
// 当 Activity 回到前台时,通知服务开始拦截键盘事件。
11071125
KeyboardAccessibilityService.setIntercepting(true);
11081126

@@ -1802,8 +1820,28 @@ protected void onDestroy() {
18021820
}
18031821
}
18041822

1823+
private boolean isCursorRestored = false;
1824+
1825+
/**
1826+
* 封装恢复逻辑:确保只执行一次
1827+
*/
1828+
public void restoreCursorIfNecessary() {
1829+
if (!isCursorRestored) {
1830+
if (prefConfig.cursorAutoShow && cursorVisible && conn != null && connected) {
1831+
switchRemoteCursorShow();
1832+
saveCursorState(1); // 标记为已恢复/待处理
1833+
}
1834+
isCursorRestored = true; // 锁定,防止重复执行
1835+
}
1836+
}
1837+
18051838
@Override
18061839
protected void onPause() {
1840+
// 本地光标下,结束串流自动恢复光标显示
1841+
// 只有在非销毁(按 Home 键或切应用)的情况下尝试自动恢复
1842+
// 如果是通过按钮退出的,标志位会提前变为 true,这里就不会再跑一次
1843+
restoreCursorIfNecessary();
1844+
18071845
// 当 Activity 进入后台时,必须停止拦截,否则会影响手机的正常使用!
18081846
KeyboardAccessibilityService.setIntercepting(false);
18091847

@@ -1830,6 +1868,13 @@ protected void onPause() {
18301868
protected void onStop() {
18311869
super.onStop();
18321870

1871+
// 暂停串流时长计时(进入后台时串流实际上是暂停的)
1872+
if (isStreamingActive && lastActiveTime > 0) {
1873+
accumulatedStreamTime += System.currentTimeMillis() - lastActiveTime;
1874+
isStreamingActive = false;
1875+
LimeLog.info("串流时长计时暂停,已累计: " + (accumulatedStreamTime / 1000) + " 秒");
1876+
}
1877+
18331878
// 检查是否是因为进入后台(包括锁屏、滑到任务栏、Home键)导致的应用停止
18341879
// 只要 Activity 不是正在 Finishing(即不是用户点了退出或崩溃),且开启了快速恢复,就标记为需要恢复
18351880
if (!shouldResumeSession && !isFinishing()) {
@@ -1928,7 +1973,15 @@ protected void onStop() {
19281973

19291974
// 记录游戏流媒体结束事件
19301975
if (analyticsManager != null && pcName != null && streamStartTime > 0) {
1931-
long streamDuration = System.currentTimeMillis() - streamStartTime;
1976+
// 计算精确的有效串流时长
1977+
// = 已累计的时间 + 当前活跃段时间(如果当前是活跃状态)
1978+
long effectiveStreamDuration = accumulatedStreamTime;
1979+
if (isStreamingActive && lastActiveTime > 0) {
1980+
effectiveStreamDuration += System.currentTimeMillis() - lastActiveTime;
1981+
}
1982+
1983+
// 同时记录总耗时(包括后台暂停时间)用于对比
1984+
long totalElapsedTime = System.currentTimeMillis() - streamStartTime;
19321985

19331986
// 收集性能数据
19341987
int resolutionWidth = 0;
@@ -1943,9 +1996,17 @@ protected void onStop() {
19431996
averageDecoderLatency = decoderRenderer.getAverageDecoderLatency();
19441997
}
19451998

1946-
analyticsManager.logGameStreamEnd(pcName, appName, streamDuration,
1999+
// 使用有效串流时长进行统计
2000+
analyticsManager.logGameStreamEnd(pcName, appName, effectiveStreamDuration,
19472001
decoderMessage, resolutionWidth, resolutionHeight,
19482002
averageEndToEndLatency, averageDecoderLatency);
2003+
2004+
LimeLog.info("串流统计 - 有效时长: " + (effectiveStreamDuration / 1000) + "秒, 总耗时: " + (totalElapsedTime / 1000) + "秒");
2005+
2006+
// 重置统计状态
2007+
streamStartTime = 0;
2008+
accumulatedStreamTime = 0;
2009+
isStreamingActive = false;
19492010
}
19502011

19512012
if (shouldResumeSession) {
@@ -1977,6 +2038,40 @@ private void setInputGrabState(boolean grab) {
19772038

19782039
private final Runnable toggleGrab = () -> setInputGrabState(!grabbedInput);
19792040

2041+
private void switchRemoteCursorShow() {
2042+
final short[] keys = {
2043+
KeyboardTranslator.VK_LCONTROL,
2044+
KeyboardTranslator.VK_MENU,
2045+
KeyboardTranslator.VK_LSHIFT,
2046+
KeyboardTranslator.VK_N
2047+
};
2048+
2049+
final byte[] modifier = {0};
2050+
2051+
for (short key : keys) {
2052+
conn.sendKeyboardInput(key, KeyboardPacket.KEY_DOWN, modifier[0], (byte) 0);
2053+
modifier[0] |= getModBit(key);
2054+
}
2055+
2056+
handler.postDelayed(() -> {
2057+
for (int i = keys.length - 1; i >= 0; i--) {
2058+
short key = keys[i];
2059+
modifier[0] &= ~getModBit(key);
2060+
conn.sendKeyboardInput(key, KeyboardPacket.KEY_UP, modifier[0], (byte) 0);
2061+
}
2062+
}, 25L);
2063+
}
2064+
2065+
private byte getModBit(short key) {
2066+
switch (key) {
2067+
case KeyboardTranslator.VK_LSHIFT: return KeyboardPacket.MODIFIER_SHIFT;
2068+
case KeyboardTranslator.VK_LCONTROL: return KeyboardPacket.MODIFIER_CTRL;
2069+
case KeyboardTranslator.VK_LWIN: return KeyboardPacket.MODIFIER_META;
2070+
case KeyboardTranslator.VK_MENU: return KeyboardPacket.MODIFIER_ALT;
2071+
default: return 0;
2072+
}
2073+
}
2074+
19802075
// Returns true if the key stroke was consumed
19812076
private boolean handleSpecialKeys(int androidKeyCode, boolean down) {
19822077
int modifierMask = 0;
@@ -2035,10 +2130,17 @@ private boolean handleSpecialKeys(int androidKeyCode, boolean down) {
20352130
grabbedInput = true;
20362131
}
20372132
cursorVisible = !cursorVisible;
2038-
if (cursorVisible) {
2039-
inputCaptureProvider.showCursor();
2040-
} else {
2041-
inputCaptureProvider.hideCursor();
2133+
// 切换本地光标时自动切换远程鼠标可见性
2134+
if (prefConfig.cursorAutoShow && conn != null && connected) {
2135+
switchRemoteCursorShow();
2136+
}
2137+
if (inputCaptureProvider.isCapturingEnabled()){
2138+
Toast.makeText(this, "切换为本地鼠标模式", Toast.LENGTH_SHORT).show();
2139+
inputCaptureProvider.disableCapture();
2140+
}
2141+
else {
2142+
Toast.makeText(this, "切换为远程鼠标模式", Toast.LENGTH_SHORT).show();
2143+
inputCaptureProvider.enableCapture();
20422144
}
20432145
break;
20442146

@@ -2556,11 +2658,17 @@ public void toggleKeyboard() {
25562658
/**
25572659
* 启用或禁用安卓本地鼠标指针
25582660
*/
2559-
public void enableNativeMousePointer(boolean enable) {
2661+
public void enableNativeMousePointer(boolean enable, boolean autoSwitch) {
25602662
LimeLog.info("Setting native mouse pointer: " + enable);
25612663

25622664
prefConfig.enableNativeMousePointer = enable;
25632665

2666+
if (autoSwitch && cursorVisible != enable) {
2667+
if (prefConfig.cursorAutoShow && conn != null && connected) {
2668+
switchRemoteCursorShow();
2669+
}
2670+
}
2671+
25642672
if (enable) {
25652673
// 启用本地鼠标指针:释放鼠标捕获但保持键盘捕获
25662674
inputCaptureProvider.disableCapture();
@@ -3123,7 +3231,7 @@ else if (eventSource == InputDevice.SOURCE_MOUSE_RELATIVE &&
31233231
int deviceSources = event.getDevice() != null ? event.getDevice().getSources() : 0;
31243232

31253233
// 本地鼠标指针模式的特殊处理
3126-
if (prefConfig.enableNativeMousePointer && (eventSource & InputDevice.SOURCE_CLASS_POINTER) != 0) {
3234+
if (cursorVisible && (eventSource & InputDevice.SOURCE_CLASS_POINTER) != 0) {
31273235
// 检查是否为真正的鼠标设备(而不是触摸屏)
31283236
boolean isActualMouse = (eventSource == InputDevice.SOURCE_MOUSE) ||
31293237
(eventSource == InputDevice.SOURCE_MOUSE_RELATIVE) ||
@@ -3887,9 +3995,25 @@ public void connectionStarted() {
38873995
Handler h = new Handler();
38883996
h.postDelayed(() -> {
38893997
// 根据配置决定是否启用原生鼠标指针
3998+
int state = getCursorState();
38903999
if (prefConfig.enableNativeMousePointer) {
3891-
enableNativeMousePointer(true);
4000+
// 使用本地鼠标时,连接开始自动关闭远程光标显示
4001+
if (state == 1) {
4002+
// 状态为 1,说明上次退出或暂停时重新打开了远程光标显示,现在需要关闭
4003+
if (prefConfig.cursorAutoShow) {
4004+
switchRemoteCursorShow();
4005+
saveCursorState(0); // 恢复后重置状态为 0
4006+
}
4007+
}
4008+
enableNativeMousePointer(true, false);
38924009
} else {
4010+
if (state == 0) {
4011+
// 状态为 0,说明上次意外退出,已经是关闭状态,现在因为设置改变需要开启
4012+
if (prefConfig.cursorAutoShow) {
4013+
switchRemoteCursorShow();
4014+
}
4015+
saveCursorState(1);
4016+
}
38934017
setInputGrabState(true);
38944018
}
38954019
}, 500);
@@ -3955,8 +4079,13 @@ public void onPermissionRequested() {
39554079
});
39564080
}
39574081

3958-
// 记录游戏流媒体开始事件
4082+
// 初始化串流时长统计
39594083
streamStartTime = System.currentTimeMillis();
4084+
accumulatedStreamTime = 0;
4085+
lastActiveTime = streamStartTime;
4086+
isStreamingActive = true;
4087+
4088+
// 记录游戏流媒体开始事件
39604089
if (analyticsManager != null && pcName != null) {
39614090
analyticsManager.logGameStreamStart(pcName, appName);
39624091
}
@@ -3972,6 +4101,13 @@ public void onPermissionRequested() {
39724101
protected void onStart() {
39734102
super.onStart();
39744103

4104+
// 恢复串流时长计时(从后台恢复时)
4105+
if (!isStreamingActive && streamStartTime > 0) {
4106+
lastActiveTime = System.currentTimeMillis();
4107+
isStreamingActive = true;
4108+
LimeLog.info("串流时长计时恢复,之前累计: " + (accumulatedStreamTime / 1000) + " 秒");
4109+
}
4110+
39754111
if (shouldResumeSession) {
39764112
LimeLog.info("从后台恢复,正在快速重连...");
39774113

@@ -4308,7 +4444,7 @@ private void initializeLocalCursorRenderers(int width, int height) {
43084444
// 3. 必须没开启原生鼠标 (防止冲突)
43094445
boolean shouldShow = prefConfig.enableLocalCursorRendering
43104446
&& prefConfig.touchscreenTrackpad
4311-
&& !prefConfig.enableNativeMousePointer;
4447+
&& !cursorVisible;
43124448

43134449
relativeContext.setEnableLocalCursorRendering(shouldShow);
43144450
}
@@ -4329,7 +4465,7 @@ private void destroyLocalCursorRenderers() {
43294465
}
43304466

43314467
public void refreshLocalCursorState(boolean enabled) {
4332-
boolean shouldRender = enabled && !prefConfig.enableNativeMousePointer;
4468+
boolean shouldRender = enabled && !cursorVisible;
43334469

43344470
for (TouchContext context : relativeTouchContextMap) {
43354471
if (context instanceof RelativeTouchContext) {
@@ -4508,7 +4644,7 @@ private void startCursorService(String hostIp) {
45084644
if (targetBitmap != null) {
45094645
final android.graphics.Bitmap finalBmp = targetBitmap;
45104646
runOnUiThread(() -> {
4511-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && prefConfig.enableNativeMousePointer) {
4647+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cursorVisible) {
45124648
// 方案B:当启用了原生指针且API版本符合时,使用 PointerIcon
45134649
PointerIcon pointerIcon = PointerIcon.create(finalBmp, hotX, hotY);
45144650
streamView.setPointerIcon(pointerIcon);
@@ -4537,7 +4673,7 @@ private void startCursorService(String hostIp) {
45374673
lastReceiveTime = System.currentTimeMillis(); // 重置计时,避免疯狂触发
45384674

45394675
runOnUiThread(() -> {
4540-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && prefConfig.enableNativeMousePointer) {
4676+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cursorVisible) {
45414677
// 恢复为默认箭头
45424678
streamView.setPointerIcon(PointerIcon.getSystemIcon(Game.this, PointerIcon.TYPE_ARROW));
45434679
} else {

0 commit comments

Comments
 (0)