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
@@ -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"