9292import android .widget .TextView ;
9393import android .widget .Toast ;
9494
95- import androidx .annotation .RequiresApi ;
96-
9795import java .io .ByteArrayInputStream ;
9896import java .lang .reflect .InvocationTargetException ;
9997import 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