glasses: list all installed launchable apps on phone (Android 11+ queries)
- Add <intent MAIN+LAUNCHER> to <queries> so queryIntentActivities on the glasses side returns third-party apps (free.zona etc), not only system / RayNeo system apps. Without this entry Android 11+ package visibility filter strips apps whose package is not explicitly named. - Pass MATCH_ALL | GET_RESOLVED_FILTER to queryIntentActivities as a belt-and-suspenders measure (no visible effect with queries above, but safe on Android 12+ where the platform may still filter). - Default VmGlassesController state is NoAppSelected instead of Starting, so we don't show 'Подготавливаем очки...' on first launch when no LaunchAppCommand has been received yet. - DriveShizukuState leaves Running state alone on re-check (don't drop a running guest back to ShizukuMissing just because Shizuku was re-polled).
This commit is contained in:
@@ -89,9 +89,13 @@ private fun DriveShizukuState(controller: VmGlassesController, shizuku: ShizukuB
|
||||
ShizukuBootstrap.AllowState.NotRunning,
|
||||
ShizukuBootstrap.AllowState.Waiting,
|
||||
ShizukuBootstrap.AllowState.NeedsPermission -> {
|
||||
controller.setState(GlassesScreenState.ShizukuMissing)
|
||||
if (controller.state.value !is GlassesScreenState.Running) {
|
||||
controller.setState(GlassesScreenState.ShizukuMissing)
|
||||
}
|
||||
}
|
||||
ShizukuBootstrap.AllowState.Granted -> {
|
||||
// Если мы сейчас в ShizukuMissing — переходим в NoAppSelected.
|
||||
// Running не трогаем: приложение уже запущено, Shizuku-статус не должен его ронять.
|
||||
if (controller.state.value is GlassesScreenState.ShizukuMissing) {
|
||||
controller.setState(GlassesScreenState.NoAppSelected)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import pw.binom.rayneovm.shared.Shutdown
|
||||
import pw.binom.rayneovm.shared.TouchEvent
|
||||
import pw.binom.rayneovm.shared.VmMessage
|
||||
import pw.binom.rayneovm.shared.VmProtocol
|
||||
import pw.binom.rayneovm.shared.GetAppList
|
||||
import pw.binom.rayneovm.shared.AppEntry
|
||||
import pw.binom.rayneovm.shared.AppListResponse
|
||||
|
||||
/**
|
||||
* Foreground service на стороне очков.
|
||||
@@ -111,16 +114,28 @@ class VmService : LifecycleService() {
|
||||
when (msg) {
|
||||
is LaunchAppCommand -> {
|
||||
Log.i(TAG, "LaunchAppCommand: ${msg.packageName}")
|
||||
// Реальный запуск — через Shizuku service (см. VmShizukuService).
|
||||
// Пока — переключаем состояние для UI.
|
||||
controller.setState(GlassesScreenState.Running(msg.packageName))
|
||||
// Реальный запуск: VirtualDisplay + launchActivity через Shizuku.
|
||||
// acquireFrameSource сам освобождает предыдущий VirtualDisplay, если был.
|
||||
controller.launchGuest(msg.packageName)
|
||||
}
|
||||
is TouchEvent -> Log.d(TAG, "Touch: ${msg.actions.size}")
|
||||
is VmKeyEvent -> Log.d(TAG, "Key: ${msg.keyCode}")
|
||||
is SetAudioMode -> Log.d(TAG, "SetAudioMode: ${msg.mode}")
|
||||
is Shutdown -> {
|
||||
Log.i(TAG, "Shutdown")
|
||||
controller.setState(GlassesScreenState.NoAppSelected)
|
||||
controller.stopGuest()
|
||||
}
|
||||
is GetAppList -> {
|
||||
Log.i(TAG, "GetAppList")
|
||||
val apps = listInstalledLaunchableApps()
|
||||
val response = AppListResponse(apps = apps)
|
||||
val json = VmProtocol.encode(response)
|
||||
try {
|
||||
server!!.send(connId, json)
|
||||
Log.i(TAG, "Sent app list: ${apps.size} apps")
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "Failed to send app list", t)
|
||||
}
|
||||
}
|
||||
is Ping -> server?.send(connId, VmProtocol.encode(Pong))
|
||||
else -> Log.d(TAG, "Unhandled: $msg")
|
||||
@@ -218,6 +233,23 @@ class VmService : LifecycleService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun listInstalledLaunchableApps(): List<AppEntry> {
|
||||
val pm = packageManager
|
||||
val mainIntent = android.content.Intent(android.content.Intent.ACTION_MAIN)
|
||||
.addCategory(android.content.Intent.CATEGORY_LAUNCHER)
|
||||
// MATCH_ALL: на Android 11+ без `<queries>` для конкретного пакета обычные флаги
|
||||
// могут не возвращать часть приложений. free.zona — пример.
|
||||
val flags = android.content.pm.PackageManager.MATCH_ALL or
|
||||
android.content.pm.PackageManager.GET_RESOLVED_FILTER
|
||||
val resolved = pm.queryIntentActivities(mainIntent, flags)
|
||||
return resolved.map { ri ->
|
||||
AppEntry(
|
||||
packageName = ri.activityInfo.packageName,
|
||||
displayName = ri.loadLabel(pm).toString(),
|
||||
)
|
||||
}.sortedBy { it.displayName.lowercase() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WS_PORT = 8080
|
||||
const val WS_PATH = "/vm"
|
||||
|
||||
+78
-11
@@ -26,7 +26,7 @@ class VmGlassesController {
|
||||
|
||||
private val testScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val _state = MutableStateFlow<GlassesScreenState>(GlassesScreenState.Starting)
|
||||
private val _state = MutableStateFlow<GlassesScreenState>(GlassesScreenState.NoAppSelected)
|
||||
val state: StateFlow<GlassesScreenState> = _state.asStateFlow()
|
||||
|
||||
private val _frameSource = MutableStateFlow<FrameSource?>(null)
|
||||
@@ -40,8 +40,7 @@ class VmGlassesController {
|
||||
Log.w(TAG, "acquireFrameSource: appContext not set")
|
||||
return null
|
||||
}
|
||||
activeSource?.release()
|
||||
activeSource = null
|
||||
releaseFrameSource()
|
||||
val surfaceTexture = try {
|
||||
SurfaceTexture(true).also {
|
||||
it.setDefaultBufferSize(VIRTUAL_WIDTH, VIRTUAL_HEIGHT)
|
||||
@@ -50,18 +49,39 @@ class VmGlassesController {
|
||||
Log.e(TAG, "SurfaceTexture creation failed", t)
|
||||
return null
|
||||
}
|
||||
val source = VirtualDisplayFrameSource.acquire(
|
||||
context = ctx,
|
||||
surfaceTexture = surfaceTexture,
|
||||
serviceClass = VmShizukuService::class.java,
|
||||
width = VIRTUAL_WIDTH,
|
||||
height = VIRTUAL_HEIGHT,
|
||||
density = VIRTUAL_DENSITY,
|
||||
)
|
||||
val source = try {
|
||||
VirtualDisplayFrameSource.acquire(
|
||||
context = ctx,
|
||||
surfaceTexture = surfaceTexture,
|
||||
serviceClass = VmShizukuService::class.java,
|
||||
width = VIRTUAL_WIDTH,
|
||||
height = VIRTUAL_HEIGHT,
|
||||
density = VIRTUAL_DENSITY,
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "VirtualDisplayFrameSource.acquire failed for $packageName", t)
|
||||
return null
|
||||
}
|
||||
activeSource = source
|
||||
_frameSource.value = source
|
||||
return source
|
||||
}
|
||||
|
||||
/** Освобождает активный VirtualDisplay и очищает FrameSource. */
|
||||
fun releaseFrameSource() {
|
||||
val src = activeSource ?: run {
|
||||
_frameSource.value = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
src.release()
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "releaseFrameSource failed", t)
|
||||
}
|
||||
activeSource = null
|
||||
_frameSource.value = null
|
||||
}
|
||||
|
||||
fun setState(newState: GlassesScreenState) {
|
||||
Log.i(TAG, "setState: $newState")
|
||||
_state.value = newState
|
||||
@@ -85,6 +105,53 @@ class VmGlassesController {
|
||||
}
|
||||
}
|
||||
|
||||
/** Запустить гостя через VirtualDisplay (вызывается из VmService по LaunchAppCommand). */
|
||||
fun launchGuest(packageName: String) {
|
||||
Log.i(TAG, "launchGuest: $packageName")
|
||||
setState(GlassesScreenState.Running(packageName))
|
||||
|
||||
// Шаг 1: запустить гостя на основном дисплее через обычный startActivity.
|
||||
// Это работает без системных прав — обычный Android API.
|
||||
// Пока без 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 {
|
||||
val src = acquireFrameSource(packageName)
|
||||
if (src == null) {
|
||||
Log.w(TAG, "launchGuest: FrameSource не получен (Shizuku?), гость работает на основном дисплее")
|
||||
} else {
|
||||
Log.i(TAG, "launchGuest: FrameSource активен для $packageName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Остановить гостя и освободить VirtualDisplay (по Shutdown или смене приложения). */
|
||||
fun stopGuest() {
|
||||
Log.i(TAG, "stopGuest")
|
||||
releaseFrameSource()
|
||||
setState(GlassesScreenState.NoAppSelected)
|
||||
}
|
||||
|
||||
fun setAppContext(context: Context) {
|
||||
this.appContext = context.applicationContext
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user