feat(android): 状态栏触摸诊断通道(vivo logcat 被限流的对策)

本机(vivo S6 + 办公 Windows)logcat 只能抓到 4 行,App 日志完全不可靠,
排查触摸/坐标问题时没有任何可用通道。

- RemoteSurfaceView 加 onDebug 回调:ACTION_DOWN/MOVE/UP 的视图坐标与
  发出的 norm 归一化坐标实时上报
- MainActivity 统一 refreshStatus():连接态 + 帧数/RTT + 最近触摸合并成
  一处刷新(原来 8 处各自 statusView.text 互相覆盖,且引用过已删的 showHint)
- 删掉临时步骤标记与 lastMeasureInfo(诊断价值已被 onDebug 覆盖)

实测:状态栏正确显示 UP view=(643,222) norm=(39017,23968) 1080x607,
据此确认了 letterbox 视图尺寸与归一化换算全对,把排查焦点正确引向
agent 注入侧(VIRTUALDESK,已另行修复)。
This commit is contained in:
郑博文 2026-10-30 10:35:21 +08:00
parent 6a6f989595
commit fe558a1c1e
2 changed files with 89 additions and 31 deletions

View File

@ -37,11 +37,17 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
@Volatile @Volatile
private var remoteH = 0 private var remoteH = 0
/** 最近一次触摸事件(诊断用:logcat 被限流,状态栏是唯一可靠通道) */
@Volatile
private var lastTouchDbg = "-"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.i(TAG, "onCreate start")
surfaceView = RemoteSurfaceView(this).apply { surfaceView = RemoteSurfaceView(this).apply {
listener = this@MainActivity listener = this@MainActivity
onDebug = { msg -> lastTouchDbg = msg; refreshStatus() }
holder.addCallback(object : SurfaceHolder.Callback { holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {} override fun surfaceCreated(holder: SurfaceHolder) {}
override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {} override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {}
@ -58,14 +64,20 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
setTextColor(0xFFFFFFFF.toInt()) setTextColor(0xFFFFFFFF.toInt())
} }
// 画面居中、黑边填充:LinearLayout 竖直 + gravity center // 画面居中、黑边填充。
// 用 FrameLayout + MATCH_PARENT(不是 WRAP_CONTENT):FrameLayout 会把
// 子视图的 WRAP_CONTENT 当 MATCH_PARENT 处理,onMeasure 里拿到的
// MeasureSpec 就成了 EXACTLY 全尺寸,letterbox 比例收缩被直接忽略
// (2026-10-30 实测:视图被撑成 1080x2001,触摸按 2001 归一化 → 坐标全错)。
// MATCH_PARENT + gravity=center 时父容器传 AT_MOST,onMeasure 的
// setMeasuredDimension 才真正决定尺寸,再被 gravity 居中摆放。
val stage = android.widget.FrameLayout(this).apply { val stage = android.widget.FrameLayout(this).apply {
setBackgroundColor(0xFF000000.toInt()) setBackgroundColor(0xFF000000.toInt())
addView( addView(
surfaceView, surfaceView,
android.widget.FrameLayout.LayoutParams( android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
android.view.Gravity.CENTER android.view.Gravity.CENTER
) )
) )
@ -74,23 +86,33 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
orientation = android.widget.LinearLayout.VERTICAL orientation = android.widget.LinearLayout.VERTICAL
addView(stage, android.widget.LinearLayout.LayoutParams( addView(stage, android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)) android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f))
// 固定高度而非 WRAP_CONTENT:与 weight=1 的兄弟共存时,
// WRAP_CONTENT 可能被压成 0(实测 bounds=[0,0][0,0],状态栏不可见)
addView(statusView, android.widget.LinearLayout.LayoutParams( addView(statusView, android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)) (48 * resources.displayMetrics.density).toInt()))
} }
setContentView(root) setContentView(root)
refreshStatus()
showHint()
intent?.getStringExtra("host")?.let { host -> intent?.getStringExtra("host")?.let { host ->
connect(host, intent.getIntExtra("port", 47800), intent.getStringExtra("code") ?: "123456") connect(host, intent.getIntExtra("port", 47800), intent.getStringExtra("code") ?: "123456")
} }
} }
private fun showHint() { /** 状态栏统一刷新:连接态 + 帧数/RTT + 最近触摸诊断(logcat 被限流,这是唯一通道) */
statusView.text = if (handle != 0L) { @Volatile
"已连接 ${remoteW}x$remoteH|单指=左键 双指轻点=右键 双指滑动=滚轮 返回键=断开" private var lastFrames = 0L
} else { @Volatile
"未连接|am start -n com.verify.remote/.MainActivity --es host <IP> --ei port 47800 --es code t" private var lastRtt = 0L
private fun refreshStatus() {
if (handle == 0L) {
statusView.text = "未连接|am start ... --es host <IP> --ei port 47800 --es code t"
return
}
statusView.text = buildString {
append("已连接 ${remoteW}x$remoteH 帧=$lastFrames RTT=${lastRtt}ms\n")
append("触摸: $lastTouchDbg")
} }
} }
@ -135,8 +157,10 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
remoteW = w remoteW = w
remoteH = ht remoteH = ht
runOnUiThread { runOnUiThread {
if (w > 0 && ht > 0) surfaceView.videoAspectRatio = w.toFloat() / ht if (w > 0 && ht > 0) {
showHint() surfaceView.videoAspectRatio = w.toFloat() / ht
}
refreshStatus()
startDecodeLoop(h) startDecodeLoop(h)
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -173,19 +197,14 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
} }
frames++ frames++
if (frames % 120 == 0L) { if (frames % 120 == 0L) {
val rtt = RustBridge.rttMs(h) lastFrames = frames
runOnUiThread { lastRtt = RustBridge.rttMs(h)
statusView.text = runOnUiThread { refreshStatus() }
"已连接 ${remoteW}x$remoteH 帧=$frames RTT=${rtt}ms|返回键断开"
}
} }
} }
dec.release() dec.release()
Log.i(TAG, "decode loop ended, frames=$frames") Log.i(TAG, "decode loop ended, frames=$frames")
runOnUiThread { runOnUiThread { refreshStatus() }
statusView.text = "会话结束(帧 $frames)"
showHint()
}
} }
} }
@ -200,8 +219,10 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
handle = 0L handle = 0L
remoteW = 0 remoteW = 0
remoteH = 0 remoteH = 0
lastFrames = 0
lastRtt = 0
RustBridge.destroy(h) RustBridge.destroy(h)
showHint() refreshStatus()
} }
override fun onDestroy() { override fun onDestroy() {

View File

@ -62,15 +62,33 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
isFocusable = true isFocusable = true
} }
/**
* letterbox 测量:在父容器给出的可用空间内按视频宽高比取最大内接矩形。
*
* 关键:父容器给 WRAP_CONTENT 时 MeasureSpec 是 AT_MOST,getSize() 是
* **上限**而非实际可用尺寸。据此算出的尺寸必须自己 setMeasuredDimension,
* 否则父容器会把视图撑满(2026-10-30 实测:视图变成 1080x2001 而非
* 1080x607,触摸按 2001 归一化 → 坐标全错,点中心落到画面外)。
*/
override fun onMeasure(widthSpec: Int, heightSpec: Int) { override fun onMeasure(widthSpec: Int, heightSpec: Int) {
super.onMeasure(widthSpec, heightSpec)
val ar = videoAspectRatio val ar = videoAspectRatio
if (ar <= 0f) return if (ar <= 0f) {
var w = measuredWidth super.onMeasure(widthSpec, heightSpec)
var h = measuredHeight return
if (w <= 0 || h <= 0) return }
if (w.toFloat() > h * ar) w = (h * ar).toInt() else h = (w / ar).toInt() val availW = MeasureSpec.getSize(widthSpec)
setMeasuredDimension(w, h) val availH = MeasureSpec.getSize(heightSpec)
if (availW <= 0 || availH <= 0) {
super.onMeasure(widthSpec, heightSpec)
return
}
var w = availW
var h = (availW / ar).toInt()
if (h > availH) {
h = availH
w = (availH * ar).toInt()
}
setMeasuredDimension(w.coerceAtLeast(1), h.coerceAtLeast(1))
} }
private fun normX(x: Float) = (x / width * 65535).toInt().coerceIn(0, 65535) private fun normX(x: Float) = (x / width * 65535).toInt().coerceIn(0, 65535)
@ -85,9 +103,28 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
private fun midY(e: MotionEvent) = private fun midY(e: MotionEvent) =
if (e.pointerCount >= 2) (e.getY(0) + e.getY(1)) / 2f else e.y if (e.pointerCount >= 2) (e.getY(0) + e.getY(1)) / 2f else e.y
/** 诊断回调:把触摸情况显示到状态栏(本机 logcat 被系统限流,日志不可靠) */
var onDebug: ((String) -> Unit)? = null
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
val l = listener ?: return false val l = listener ?: run {
onDebug?.invoke("touch: listener=null")
return false
}
if (width == 0 || height == 0) return false if (width == 0 || height == 0) return false
val act = when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> "DOWN"
MotionEvent.ACTION_MOVE -> "MOVE"
MotionEvent.ACTION_UP -> "UP"
MotionEvent.ACTION_POINTER_DOWN -> "P2DOWN"
MotionEvent.ACTION_POINTER_UP -> "P2UP"
MotionEvent.ACTION_CANCEL -> "CANCEL"
else -> event.actionMasked.toString()
}
onDebug?.invoke(
"$act view=(${event.x.toInt()},${event.y.toInt()}) " +
"norm=(${normX(event.x)},${normY(event.y)}) ${width}x$height"
)
when (event.actionMasked) { when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {