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:
2026-09-25 15:19:57 +03:00
parent ca01a7b918
commit cc5866b373
4 changed files with 130 additions and 19 deletions
+11 -3
View File
@@ -24,14 +24,22 @@
android:name="android.hardware.type.embedded"
android:required="true" />
<!-- Android 11+ package visibility: Shizuku API выполняет context.bindService на сервис
внутри пакета moe.shizuku.privileged.api; без явного queries bindService возвращает
false и binder к нам никогда не приедет. -->
<!-- Android 11+ package visibility:
- Shizuku API выполняет context.bindService на сервис внутри пакета
moe.shizuku.privileged.api; без явного queries bindService возвращает false
и binder к нам никогда не приедет.
- queryIntentActivities(MAIN+LAUNCHER) без intent-queries возвращает только
системные/RayNeo приложения; для всех сторонних (free.zona и т.п.)
нужен явный <intent> с ACTION_MAIN + CATEGORY_LAUNCHER. -->
<queries>
<package android:name="moe.shizuku.privileged.api" />
<intent>
<action android:name="rikka.shizuku.intent.action.REQUEST_BINDER" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
<application
@@ -89,9 +89,13 @@ private fun DriveShizukuState(controller: VmGlassesController, shizuku: ShizukuB
ShizukuBootstrap.AllowState.NotRunning,
ShizukuBootstrap.AllowState.Waiting,
ShizukuBootstrap.AllowState.NeedsPermission -> {
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"
@@ -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,7 +49,8 @@ class VmGlassesController {
Log.e(TAG, "SurfaceTexture creation failed", t)
return null
}
val source = VirtualDisplayFrameSource.acquire(
val source = try {
VirtualDisplayFrameSource.acquire(
context = ctx,
surfaceTexture = surfaceTexture,
serviceClass = VmShizukuService::class.java,
@@ -58,10 +58,30 @@ class VmGlassesController {
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
}