Feat: TV remote control via D-pad focus navigation using AccessibilityService

This commit is contained in:
yuanyuanxiang
2026-06-22 17:57:51 +02:00
parent 45553ec5b6
commit 218ee4f43d
22 changed files with 631 additions and 67 deletions

View File

@@ -21,6 +21,7 @@ import android.provider.Settings
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import android.widget.Toast
class CaptureService : Service() {
@@ -39,6 +40,14 @@ class CaptureService : Service() {
// CS-01 fix: 通过 idrHandler.post 序列化到主线程,避免 TOCTOU 竞争
@Volatile var instance: CaptureService? = null
@JvmStatic
fun onNativeStatus(msg: String) {
val svc = instance ?: return
svc.idrHandler.post {
Toast.makeText(svc, msg, Toast.LENGTH_LONG).show()
}
}
@JvmStatic
fun requestIdr() {
val svc = instance ?: return
@@ -100,9 +109,16 @@ class CaptureService : Service() {
super.onCreate()
instance = this
createNotificationChannel()
// CS-06: start with DATA_SYNC type only; upgrade to MEDIA_PROJECTION in
// onStartCommand when the token is actually available. On Android 14+
// (targetSdk 34+) calling startForeground(MEDIA_PROJECTION) without
// immediately pairing it with getMediaProjection() causes the system to
// kill the service after a ~5-second grace period — the root cause of the
// 3-9 s disconnect seen on Google TV when no screen-capture permission
// was granted.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIF_ID, buildNotification())
}
@@ -124,12 +140,23 @@ class CaptureService : Service() {
Log.i(TAG, "CaptureService: server=$ip:$port device=$model res=$res")
// YB-01 fix: so 库加载失败是 Error不被 catch(Exception) 捕获,需单独处理
// ExceptionInInitializerError 包裹 UnsatisfiedLinkError首次访问 object 时触发)
val initRet: Int
try {
YamaBridge.nativeInit(ip, port, androidId, model, osVer, res, user, packageCodePath ?: "")
initRet = YamaBridge.nativeInit(ip, port, androidId, model, osVer, res, user, packageCodePath ?: "")
} catch (e: UnsatisfiedLinkError) {
Log.e(TAG, "Native library load failed: $e")
stopSelf()
return START_NOT_STICKY
} catch (e: ExceptionInInitializerError) {
Log.e(TAG, "Native library init failed: ${e.cause}")
stopSelf()
return START_NOT_STICKY
}
if (initRet != 0) {
Log.e(TAG, "nativeInit failed: ret=$initRet")
stopSelf()
return START_NOT_STICKY
}
val projResult = intent.getIntExtra(EXTRA_PROJECTION_RESULT, -1)
@@ -139,12 +166,22 @@ class CaptureService : Service() {
@Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_PROJECTION_DATA)
if (projResult == android.app.Activity.RESULT_OK && projData != null) {
// Android 14+: upgrade foreground type to MEDIA_PROJECTION in the same
// start command that calls getMediaProjection(), as required by API 34+.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
}
val pm = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val mp = pm.getMediaProjection(projResult, projData)
mediaProjection = mp
// CS-05 fix: 分辨率为 0 时不启动采集,避免 MediaCodec 配置崩溃
if (sw > 0 && sh > 0) {
startCapture(mp, sw, sh)
try {
startCapture(mp, sw, sh)
} catch (e: Exception) {
Log.e(TAG, "Screen capture failed: $e — running without capture")
}
} else {
Log.e(TAG, "Invalid screen resolution ${sw}x${sh}, skipping capture")
}
@@ -223,9 +260,14 @@ class CaptureService : Service() {
// Android 14+ requires callback registered before createVirtualDisplay()
mp.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
Log.i(TAG, "MediaProjection stopped")
Log.i(TAG, "MediaProjection stopped — switching to dataSync foreground type")
stopCapture()
stopSelf()
// Android 14+: system auto-kills a foreground service whose mediaProjection
// is revoked. Re-declare as dataSync-only to survive without screen capture.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
}
}
}, null)