glasses: launch guest on Shizuku VirtualDisplay + binocular render
- VmShizukuService is a Shizuku UserService only; remove its <service> manifest entry (ServiceStarter instantiates it reflectively, an Android service entry causes ClassCastException) - VirtualDisplayFrameSource: await binder via ShizukuVdClient.bindForResult(5s), expose virtualDisplayId + shizukuBinder, stop the UserService on release - VmGlassesController: serialize acquire via Mutex, use double-buffered SurfaceTexture (DisplayManagerService rejects single-buffer), launch the guest on the VD with ShizukuVdBinder.launchActivity - GlassesScreen: render controller.frameSource (Running) directly; drop the LaunchedEffect acquire - DebugTestReceiver: register at runtime from MainActivity (background manifest receivers are blocked on Android 12+)
This commit is contained in:
@@ -68,11 +68,10 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
|
|
||||||
<!-- Shizuku UserService для системных вызовов -->
|
<!-- VmShizukuService НЕ объявляется здесь как <service> — это Shizuku UserService,
|
||||||
<service
|
который ServiceStarter создаёт через reflection в отдельном isolated-процессе.
|
||||||
android:name=".VmShizukuService"
|
Если объявить — Shizuku-сервер не сможет привести класс к IBinder
|
||||||
android:exported="true"
|
(ClassCastException в moe.shizuku.starter.ServiceStarter). -->
|
||||||
android:process=":shizuku_vd" />
|
|
||||||
|
|
||||||
<!-- Shizuku v12+ требует ShizukuProvider в манифесте — он сам вытаскивает binder
|
<!-- Shizuku v12+ требует ShizukuProvider в манифесте — он сам вытаскивает binder
|
||||||
через ContentProvider IPC и инициализирует Shizuku-клиент. Без него binder не придёт.
|
через ContentProvider IPC и инициализирует Shizuku-клиент. Без него binder не придёт.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package pw.binom.rayneovm.glasses
|
package pw.binom.rayneovm.glasses
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -26,10 +28,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
// По доке Shizuku (https://github.com/RikkaApps/Shizuku-API): подписываемся на
|
|
||||||
// listener-ы здесь, после super.onCreate, но ДО setContent. ShizukuProvider
|
|
||||||
// (rikka.shizuku.ShizukuProvider в манифесте) уже инициализировал binder через
|
|
||||||
// ContentProvider.onCreate — pingBinder() сразу вернёт true.
|
|
||||||
ShizukuBootstrap.attach(this)
|
ShizukuBootstrap.attach(this)
|
||||||
val controller = VmGlassesController.fromContext(this)
|
val controller = VmGlassesController.fromContext(this)
|
||||||
|
|
||||||
@@ -42,18 +40,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
) {
|
) {
|
||||||
val shizukuState by ShizukuBootstrap.state.collectAsState()
|
val shizukuState by ShizukuBootstrap.state.collectAsState()
|
||||||
val vmState by controller.state.collectAsState()
|
|
||||||
DriveShizukuState(controller, shizukuState)
|
DriveShizukuState(controller, shizukuState)
|
||||||
GlassesScreen(
|
GlassesScreen(
|
||||||
controller = controller,
|
controller = controller,
|
||||||
onRequestShizukuPermission = { ShizukuBootstrap.refresh() },
|
onRequestShizukuPermission = { ShizukuBootstrap.refresh() },
|
||||||
)
|
)
|
||||||
// Как только VM ушёл в Running (запущен гость) — сворачиваем свой Activity,
|
|
||||||
// чтобы SurfaceView гостя оказался поверх и был виден на очках.
|
|
||||||
// Service продолжает жить в фоне (WebSocket/mDNS не рвутся).
|
|
||||||
if (vmState is GlassesScreenState.Running) {
|
|
||||||
(this as? ComponentActivity)?.finish()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,10 +56,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
ShizukuBootstrap.refresh()
|
ShizukuBootstrap.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* По доке Shizuku, мы должны вручную передать результат диалога в Shizuku -
|
|
||||||
* иначе он не узнает, что пользователь нажал Allow.
|
|
||||||
*/
|
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
requestCode: Int,
|
requestCode: Int,
|
||||||
permissions: Array<String>,
|
permissions: Array<String>,
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ package pw.binom.rayneovm.glasses
|
|||||||
import pw.binom.mercury.shizuku.ShizukuVdService
|
import pw.binom.mercury.shizuku.ShizukuVdService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shizuku UserService для системных вызовов (VirtualDisplay, launchActivity, injectInputEvent).
|
* Shizuku UserService для системных вызовов на очках (VirtualDisplay, launchActivity,
|
||||||
|
* injectInputEvent и т.д.).
|
||||||
*
|
*
|
||||||
* Запускается в отдельном процессе :shizuku_vd через [pw.binom.mercury.shizuku.ShizukuVdClient].
|
* Запускается в отдельном isolated-процессе от имени shell UID 2000 через
|
||||||
|
* [pw.binom.mercury.shizuku.ShizukuVdClient].
|
||||||
*
|
*
|
||||||
* Методы Binder-API ([createDisplay], [launchActivity], и т.д.) сейчас — заглушки (STUB):
|
* **Не объявляется в AndroidManifest.xml** как `<service>` — Shizuku создаёт экземпляр
|
||||||
* реальная реализация через [rikka.shizuku.SystemServiceHelper] + reflection добавляется
|
* через `Class.forName().newInstance()`, никакого Android Service-lifecycle не происходит.
|
||||||
* в следующей итерации.
|
*
|
||||||
|
* Вся логика транзакций — в базовом классе [ShizukuVdService]. Наследник нужен только
|
||||||
|
* как конкретный класс для `ComponentName` + публичный no-arg конструктор.
|
||||||
*/
|
*/
|
||||||
class VmShizukuService : ShizukuVdService()
|
class VmShizukuService : ShizukuVdService()
|
||||||
|
|||||||
+77
-49
@@ -14,7 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import pw.binom.mercury.binocular.FrameSource
|
import pw.binom.mercury.binocular.FrameSource
|
||||||
|
import pw.binom.mercury.shizuku.ShizukuVdBinder
|
||||||
import pw.binom.rayneovm.glasses.VmShizukuService
|
import pw.binom.rayneovm.glasses.VmShizukuService
|
||||||
import pw.binom.rayneovm.glasses.shizuku.VirtualDisplayFrameSource
|
import pw.binom.rayneovm.glasses.shizuku.VirtualDisplayFrameSource
|
||||||
import pw.binom.rayneovm.glasses.vm.GlassesScreenState
|
import pw.binom.rayneovm.glasses.vm.GlassesScreenState
|
||||||
@@ -35,19 +38,24 @@ class VmGlassesController {
|
|||||||
private var activeSource: VirtualDisplayFrameSource? = null
|
private var activeSource: VirtualDisplayFrameSource? = null
|
||||||
@Volatile private var appContext: Context? = null
|
@Volatile private var appContext: Context? = null
|
||||||
|
|
||||||
suspend fun acquireFrameSource(packageName: String): FrameSource? {
|
/** Сериализует создание/освобождение VirtualDisplay — параллельный acquire оставил бы висеть второй UserService. */
|
||||||
|
private val acquireMutex = Mutex()
|
||||||
|
|
||||||
|
suspend fun acquireFrameSource(packageName: String): FrameSource? = acquireMutex.withLock {
|
||||||
val ctx = appContext ?: run {
|
val ctx = appContext ?: run {
|
||||||
Log.w(TAG, "acquireFrameSource: appContext not set")
|
Log.w(TAG, "acquireFrameSource: appContext not set")
|
||||||
return null
|
return@withLock null
|
||||||
}
|
}
|
||||||
releaseFrameSource()
|
releaseFrameSource()
|
||||||
val surfaceTexture = try {
|
val surfaceTexture = try {
|
||||||
SurfaceTexture(true).also {
|
// ВАЖНО: single-buffer нельзя — DisplayManagerService reject'ит
|
||||||
|
// "Surface can't be single-buffered" при создании VirtualDisplay.
|
||||||
|
SurfaceTexture(false).also {
|
||||||
it.setDefaultBufferSize(VIRTUAL_WIDTH, VIRTUAL_HEIGHT)
|
it.setDefaultBufferSize(VIRTUAL_WIDTH, VIRTUAL_HEIGHT)
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(TAG, "SurfaceTexture creation failed", t)
|
Log.e(TAG, "SurfaceTexture creation failed", t)
|
||||||
return null
|
return@withLock null
|
||||||
}
|
}
|
||||||
val source = try {
|
val source = try {
|
||||||
VirtualDisplayFrameSource.acquire(
|
VirtualDisplayFrameSource.acquire(
|
||||||
@@ -60,11 +68,11 @@ class VmGlassesController {
|
|||||||
)
|
)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(TAG, "VirtualDisplayFrameSource.acquire failed for $packageName", t)
|
Log.e(TAG, "VirtualDisplayFrameSource.acquire failed for $packageName", t)
|
||||||
return null
|
return@withLock null
|
||||||
}
|
}
|
||||||
activeSource = source
|
activeSource = source
|
||||||
_frameSource.value = source
|
_frameSource.value = source
|
||||||
return source
|
source
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Освобождает активный VirtualDisplay и очищает FrameSource. */
|
/** Освобождает активный VirtualDisplay и очищает FrameSource. */
|
||||||
@@ -88,21 +96,12 @@ class VmGlassesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DEBUG: запустить гостя напрямую (без Compose-UI).
|
* DEBUG: запустить гостя напрямую (без Compose-UI) — то же, что и [launchGuest].
|
||||||
* Используется только для тестирования cross-process Surface на устройстве.
|
* Используется для тестирования через adb broadcast.
|
||||||
*/
|
*/
|
||||||
fun startTestLaunch(packageName: String) {
|
fun startTestLaunch(packageName: String) {
|
||||||
Log.i(TAG, "startTestLaunch: $packageName")
|
Log.i(TAG, "startTestLaunch: $packageName")
|
||||||
setState(GlassesScreenState.Running(packageName))
|
launchGuest(packageName)
|
||||||
// Прямо сейчас (без ожидания Compose) запускаем создание FrameSource и activity.
|
|
||||||
testScope.launch {
|
|
||||||
try {
|
|
||||||
val src = acquireFrameSource(packageName)
|
|
||||||
Log.i(TAG, "acquireFrameSource result: $src for $packageName")
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e(TAG, "startTestLaunch: acquire failed", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Запустить гостя через VirtualDisplay (вызывается из VmService по LaunchAppCommand). */
|
/** Запустить гостя через VirtualDisplay (вызывается из VmService по LaunchAppCommand). */
|
||||||
@@ -110,37 +109,37 @@ class VmGlassesController {
|
|||||||
Log.i(TAG, "launchGuest: $packageName")
|
Log.i(TAG, "launchGuest: $packageName")
|
||||||
setState(GlassesScreenState.Running(packageName))
|
setState(GlassesScreenState.Running(packageName))
|
||||||
|
|
||||||
// Шаг 1: запустить гостя на основном дисплее через обычный startActivity.
|
// Шаг 1: создать VirtualDisplay (нужны Shizuku права) и дождаться готовности displayId.
|
||||||
// Это работает без системных прав — обычный Android API.
|
// Без Shizuku acquireFrameSource вернёт null → гость не запустится.
|
||||||
// Пока без VirtualDisplay, потом перебросим на него (нужна Shizuku).
|
|
||||||
val ctx = appContext
|
|
||||||
if (ctx == null) {
|
|
||||||
Log.w(TAG, "launchGuest: appContext не установлен")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val intent = ctx.packageManager.getLaunchIntentForPackage(packageName)
|
|
||||||
if (intent == null) {
|
|
||||||
Log.w(TAG, "launchGuest: $packageName не установлен")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
ctx.startActivity(intent)
|
|
||||||
Log.i(TAG, "launchGuest: $packageName запущен на основном дисплее")
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e(TAG, "launchGuest: не удалось запустить $packageName", t)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Шаг 2: создать VirtualDisplay (нужны Shizuku права). Если Shizuku не активен —
|
|
||||||
// FrameSource останется null и GL-слой покажет чёрный экран. Но приложение
|
|
||||||
// уже запущено на основном дисплее очков, и пользователь его увидит.
|
|
||||||
testScope.launch {
|
testScope.launch {
|
||||||
val src = acquireFrameSource(packageName)
|
val src = acquireFrameSource(packageName) as? VirtualDisplayFrameSource
|
||||||
if (src == null) {
|
if (src == null) {
|
||||||
Log.w(TAG, "launchGuest: FrameSource не получен (Shizuku?), гость работает на основном дисплее")
|
Log.w(TAG, "launchGuest: FrameSource не получен (Shizuku не активен?)")
|
||||||
} else {
|
setState(GlassesScreenState.Error("Не удалось создать виртуальный экран (Shizuku?)"))
|
||||||
Log.i(TAG, "launchGuest: FrameSource активен для $packageName")
|
return@launch
|
||||||
|
}
|
||||||
|
// Шаг 2: запустить гостя на нашем виртуальном дисплее через Shizuku-UserService.
|
||||||
|
// Запуск идёт от лица shell UID — ActivityOptions.setLaunchDisplayId проходит
|
||||||
|
// проверку AMS и Activity стартует на нашем virtual display, не на основном.
|
||||||
|
// Разрешаем MainActivity на клиенте через наш PackageManager — UserService
|
||||||
|
// работает в isolated-процессе без своего PackageManager.
|
||||||
|
val ctx2 = appContext
|
||||||
|
if (ctx2 == null) {
|
||||||
|
Log.w(TAG, "launchGuest: appContext not set")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val pm = ctx2.packageManager
|
||||||
|
val launchIntent = pm.getLaunchIntentForPackage(packageName)
|
||||||
|
val className = launchIntent?.component?.className ?: ""
|
||||||
|
val ok = ShizukuVdBinder.launchActivity(
|
||||||
|
binder = src.shizukuBinder,
|
||||||
|
packageName = packageName,
|
||||||
|
className = className,
|
||||||
|
displayId = src.virtualDisplayId,
|
||||||
|
)
|
||||||
|
Log.i(TAG, "launchGuest: $packageName/$className запущен на virtualDisplayId=${src.virtualDisplayId}, ok=$ok")
|
||||||
|
if (!ok) {
|
||||||
|
setState(GlassesScreenState.Error("Не удалось запустить $packageName"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,15 +176,44 @@ interface ControllerProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DEBUG broadcast receiver для тестирования через adb:
|
* DEBUG broadcast receiver для тестирования через adb.
|
||||||
|
*
|
||||||
|
* Android 12+ блокирует доставку broadcast-ов в manifest-объявленные receiver-ы если
|
||||||
|
* процесс в фоне ("Background execution not allowed"). Поэтому регистрируем программно
|
||||||
|
* из MainActivity.onCreate() — это runtime-register receiver, exempt от background-check.
|
||||||
|
*
|
||||||
|
* Использование:
|
||||||
* adb shell am broadcast -a pw.binom.rayneovm.glasses.TEST -e package free.zona
|
* adb shell am broadcast -a pw.binom.rayneovm.glasses.TEST -e package free.zona
|
||||||
*/
|
*/
|
||||||
class DebugTestReceiver : BroadcastReceiver() {
|
class DebugTestReceiver : BroadcastReceiver() {
|
||||||
|
private var controller: VmGlassesController? = null
|
||||||
|
private var registeredContext: Context? = null
|
||||||
|
|
||||||
|
/** Вызывается из MainActivity.onCreate (когда приложение в foreground). */
|
||||||
|
fun attach(context: Context, controller: VmGlassesController) {
|
||||||
|
this.controller = controller
|
||||||
|
val filter = IntentFilter(ACTION)
|
||||||
|
// RECEIVER_EXPORTED — можно принимать от adb shell; pre-Android 13 можно было
|
||||||
|
// без флага, с 13+ обязательно указывать.
|
||||||
|
context.registerReceiver(this, filter, Context.RECEIVER_EXPORTED)
|
||||||
|
registeredContext = context
|
||||||
|
Log.i(TAG, "DebugTestReceiver registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detach(context: Context) {
|
||||||
|
try {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// not registered, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action != ACTION) return
|
if (intent.action != ACTION) return
|
||||||
val pkg = intent.getStringExtra("package") ?: return
|
val pkg = intent.getStringExtra("package") ?: return
|
||||||
Log.i(TAG, "DebugTestReceiver: launching $pkg")
|
Log.i(TAG, "DebugTestReceiver: launching $pkg")
|
||||||
val controller = (context.applicationContext as? ControllerProvider)?.provideController()
|
val controller = controller
|
||||||
|
?: (context.applicationContext as? ControllerProvider)?.provideController()
|
||||||
?: return
|
?: return
|
||||||
controller.startTestLaunch(pkg)
|
controller.startTestLaunch(pkg)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-5
@@ -25,14 +25,23 @@ class VirtualDisplayFrameSource private constructor(
|
|||||||
private val surface: Surface,
|
private val surface: Surface,
|
||||||
private val binder: IBinder,
|
private val binder: IBinder,
|
||||||
private val displayId: Int,
|
private val displayId: Int,
|
||||||
|
private val client: ShizukuVdClient,
|
||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
) : SurfaceTextureFrameSource(source, width, height) {
|
) : SurfaceTextureFrameSource(source, width, height) {
|
||||||
|
|
||||||
|
/** ID виртуального дисплея (нужен для запуска Activity через ActivityOptions.setLaunchDisplayId). */
|
||||||
|
val virtualDisplayId: Int get() = displayId
|
||||||
|
|
||||||
|
/** Binder к ShizukuVdService (для последующих вызовов launchActivity / injectInputEvent). */
|
||||||
|
val shizukuBinder: IBinder get() = binder
|
||||||
|
|
||||||
override fun release() {
|
override fun release() {
|
||||||
runCatching { ShizukuVdBinder.releaseDisplay(binder, displayId) }
|
runCatching { ShizukuVdBinder.releaseDisplay(binder, displayId) }
|
||||||
runCatching { surface.release() }
|
runCatching { surface.release() }
|
||||||
super.release()
|
super.release()
|
||||||
|
// Отвязываем UserService — держим его живым ровно на время жизни дисплея.
|
||||||
|
runCatching { client.stop() }
|
||||||
Log.i(TAG, "VirtualDisplayFrameSource released: displayId=$displayId")
|
Log.i(TAG, "VirtualDisplayFrameSource released: displayId=$displayId")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,10 +69,10 @@ class VirtualDisplayFrameSource private constructor(
|
|||||||
val surface = Surface(surfaceTexture)
|
val surface = Surface(surfaceTexture)
|
||||||
val client = ShizukuVdClient(context, serviceClass)
|
val client = ShizukuVdClient(context, serviceClass)
|
||||||
return try {
|
return try {
|
||||||
var binder: IBinder? = null
|
// bindUserService асинхронен — ждём Binder через suspend-обёртку (5с таймаут).
|
||||||
client.start { b -> binder = b }
|
val b = client.bindForResult(timeoutMs = 5000)
|
||||||
val b = binder ?: run {
|
if (b == null) {
|
||||||
Log.w(TAG, "Shizuku binder not ready, returning null")
|
Log.w(TAG, "Shizuku binder not ready (timeout или pingBinder=false), returning null")
|
||||||
surface.release()
|
surface.release()
|
||||||
client.stop()
|
client.stop()
|
||||||
return null
|
return null
|
||||||
@@ -78,7 +87,7 @@ class VirtualDisplayFrameSource private constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Acquired VirtualDisplay id=$displayId ${width}x${height}")
|
Log.i(TAG, "Acquired VirtualDisplay id=$displayId ${width}x${height}")
|
||||||
VirtualDisplayFrameSource(surfaceTexture, surface, b, displayId, width, height)
|
VirtualDisplayFrameSource(surfaceTexture, surface, b, displayId, client, width, height)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(TAG, "acquire failed", t)
|
Log.e(TAG, "acquire failed", t)
|
||||||
runCatching { surface.release() }
|
runCatching { surface.release() }
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -21,7 +20,6 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import pw.binom.mercury.binocular.BinocularRendererView
|
import pw.binom.mercury.binocular.BinocularRendererView
|
||||||
import pw.binom.mercury.binocular.FrameSource
|
|
||||||
import pw.binom.mercury.binocular.NoOpFrameSource
|
import pw.binom.mercury.binocular.NoOpFrameSource
|
||||||
import pw.binom.rayneovm.glasses.protocol.VmGlassesController
|
import pw.binom.rayneovm.glasses.protocol.VmGlassesController
|
||||||
import pw.binom.rayneovm.glasses.vm.GlassesScreenState
|
import pw.binom.rayneovm.glasses.vm.GlassesScreenState
|
||||||
@@ -42,12 +40,14 @@ fun GlassesScreen(
|
|||||||
onRequestShizukuPermission: (() -> Unit)? = null,
|
onRequestShizukuPermission: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val state by controller.state.collectAsState()
|
val state by controller.state.collectAsState()
|
||||||
val frameSource = remember { mutableStateOf<FrameSource>(NoOpFrameSource()) }
|
val controllerFrameSource by controller.frameSource.collectAsState()
|
||||||
|
val noOp = remember { NoOpFrameSource() }
|
||||||
|
val frameSource = controllerFrameSource ?: noOp
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize().background(Color.Black)) {
|
Box(modifier = modifier.fillMaxSize().background(Color.Black)) {
|
||||||
// GL-слой: всегда рисуем, но при NoOpFrameSource — просто чёрный экран
|
// GL-слой: всегда рисуем, но при NoOpFrameSource — просто чёрный экран
|
||||||
BinocularRendererView(
|
BinocularRendererView(
|
||||||
frameSource = frameSource.value,
|
frameSource = frameSource,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,12 +72,8 @@ fun GlassesScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
is GlassesScreenState.Running -> {
|
is GlassesScreenState.Running -> {
|
||||||
// Ничего поверх GL — пусть пользователь видит гостевой контент.
|
// Ничего поверх GL — пользователь видит гостевой контент.
|
||||||
// FrameSource обновляется в LaunchedEffect ниже.
|
// FrameSource уже установлен контроллером (launchGuest → controller.frameSource).
|
||||||
androidx.compose.runtime.LaunchedEffect(s.packageName) {
|
|
||||||
val src = controller.acquireFrameSource(s.packageName)
|
|
||||||
if (src != null) frameSource.value = src
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is GlassesScreenState.Error -> StatusOverlay(
|
is GlassesScreenState.Error -> StatusOverlay(
|
||||||
title = "Ошибка",
|
title = "Ошибка",
|
||||||
|
|||||||
Reference in New Issue
Block a user