feat(audio,#2): MediaProjection + AudioPlaybackCapture preview on glasses; AudioPath /vm-audio to phone; debug test tone; debug audio pipe; FGS mediaProjection type; AudioOutputSection UI toggle

This commit is contained in:
2026-09-27 06:39:00 +03:00
parent 2125788e25
commit b1efa3d462
16 changed files with 872 additions and 229 deletions
@@ -1,12 +1,16 @@
package pw.binom.rayneovm.glasses
import android.app.Activity
import android.content.Intent
import android.content.IntentFilter
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.util.Log
import android.view.Display
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -20,12 +24,41 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import pw.binom.rayneovm.glasses.audio.AudioBridge
import pw.binom.rayneovm.glasses.protocol.VmGlassesController
import pw.binom.rayneovm.glasses.ui.GlassesScreen
import pw.binom.rayneovm.glasses.vm.GlassesScreenState
class MainActivity : ComponentActivity() {
/** Последний обработанный tick запроса проекции (см. onCreate). */
private var handledTick = 0L
/**
* Пользовательский диалог MediaProjection. При выставленном appop `PROJECT_MEDIA = allow`
* он не показывается — интент сразу возвращает RESULT_OK (нужно для задачи #2).
*/
private val projectionLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val data = result.data
if (result.resultCode == Activity.RESULT_OK && data != null) {
val mp = getSystemService(MediaProjectionManager::class.java)
.getMediaProjection(result.resultCode, data)
if (mp != null) {
Log.i(TAG, "MediaProjection получен")
AudioBridge.onProjectionGranted(mp)
} else {
Log.w(TAG, "getMediaProjection вернул null")
}
} else {
Log.w(TAG, "MediaProjection не выдан: resultCode=${result.resultCode}")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// HOME живёт на display 0. Если систему (или гостя на VirtualDisplay)
@@ -38,6 +71,23 @@ class MainActivity : ComponentActivity() {
}
enableEdgeToEdge()
ShizukuBootstrap.attach(this)
// Запросы MediaProjection приходят из VmService через AudioBridge (задача #2).
// Запускаем интент только когда Activity хотя бы STARTED, иначе лончер кинет.
//
// handledTick обязателен: repeatOnLifecycle перезапускает сбор при каждом выходе
// Activity в STARTED (а после диалога проекции мы как раз возвращаемся в STARTED).
// Без него один и тот же tick запускал бы запрос повторно, новая проекция гасила
// предыдущую, и AudioRecord умирал с read -2.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
AudioBridge.requestTick.collect { tick ->
if (tick > handledTick) {
handledTick = tick
launchProjectionRequest()
}
}
}
}
val controller = VmGlassesController.fromContext(this)
setContent {
@@ -76,6 +126,17 @@ class MainActivity : ComponentActivity() {
ShizukuBootstrap.onRequestPermissionResult(requestCode, grant)
}
}
private fun launchProjectionRequest() {
runCatching {
val mpm = getSystemService(MediaProjectionManager::class.java)
projectionLauncher.launch(mpm.createScreenCaptureIntent())
}.onFailure { Log.w(TAG, "Не удалось запустить запрос MediaProjection: ${it.message}") }
}
companion object {
private const val TAG = "MainActivity"
}
}
@Composable