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:
@@ -24,14 +24,22 @@
|
|||||||
android:name="android.hardware.type.embedded"
|
android:name="android.hardware.type.embedded"
|
||||||
android:required="true" />
|
android:required="true" />
|
||||||
|
|
||||||
<!-- Android 11+ package visibility: Shizuku API выполняет context.bindService на сервис
|
<!-- Android 11+ package visibility:
|
||||||
внутри пакета moe.shizuku.privileged.api; без явного queries bindService возвращает
|
- Shizuku API выполняет context.bindService на сервис внутри пакета
|
||||||
false и binder к нам никогда не приедет. -->
|
moe.shizuku.privileged.api; без явного queries bindService возвращает false
|
||||||
|
и binder к нам никогда не приедет.
|
||||||
|
- queryIntentActivities(MAIN+LAUNCHER) без intent-queries возвращает только
|
||||||
|
системные/RayNeo приложения; для всех сторонних (free.zona и т.п.)
|
||||||
|
нужен явный <intent> с ACTION_MAIN + CATEGORY_LAUNCHER. -->
|
||||||
<queries>
|
<queries>
|
||||||
<package android:name="moe.shizuku.privileged.api" />
|
<package android:name="moe.shizuku.privileged.api" />
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="rikka.shizuku.intent.action.REQUEST_BINDER" />
|
<action android:name="rikka.shizuku.intent.action.REQUEST_BINDER" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
|||||||
@@ -89,9 +89,13 @@ private fun DriveShizukuState(controller: VmGlassesController, shizuku: ShizukuB
|
|||||||
ShizukuBootstrap.AllowState.NotRunning,
|
ShizukuBootstrap.AllowState.NotRunning,
|
||||||
ShizukuBootstrap.AllowState.Waiting,
|
ShizukuBootstrap.AllowState.Waiting,
|
||||||
ShizukuBootstrap.AllowState.NeedsPermission -> {
|
ShizukuBootstrap.AllowState.NeedsPermission -> {
|
||||||
controller.setState(GlassesScreenState.ShizukuMissing)
|
if (controller.state.value !is GlassesScreenState.Running) {
|
||||||
|
controller.setState(GlassesScreenState.ShizukuMissing)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ShizukuBootstrap.AllowState.Granted -> {
|
ShizukuBootstrap.AllowState.Granted -> {
|
||||||
|
// Если мы сейчас в ShizukuMissing — переходим в NoAppSelected.
|
||||||
|
// Running не трогаем: приложение уже запущено, Shizuku-статус не должен его ронять.
|
||||||
if (controller.state.value is GlassesScreenState.ShizukuMissing) {
|
if (controller.state.value is GlassesScreenState.ShizukuMissing) {
|
||||||
controller.setState(GlassesScreenState.NoAppSelected)
|
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.TouchEvent
|
||||||
import pw.binom.rayneovm.shared.VmMessage
|
import pw.binom.rayneovm.shared.VmMessage
|
||||||
import pw.binom.rayneovm.shared.VmProtocol
|
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 на стороне очков.
|
* Foreground service на стороне очков.
|
||||||
@@ -111,16 +114,28 @@ class VmService : LifecycleService() {
|
|||||||
when (msg) {
|
when (msg) {
|
||||||
is LaunchAppCommand -> {
|
is LaunchAppCommand -> {
|
||||||
Log.i(TAG, "LaunchAppCommand: ${msg.packageName}")
|
Log.i(TAG, "LaunchAppCommand: ${msg.packageName}")
|
||||||
// Реальный запуск — через Shizuku service (см. VmShizukuService).
|
// Реальный запуск: VirtualDisplay + launchActivity через Shizuku.
|
||||||
// Пока — переключаем состояние для UI.
|
// acquireFrameSource сам освобождает предыдущий VirtualDisplay, если был.
|
||||||
controller.setState(GlassesScreenState.Running(msg.packageName))
|
controller.launchGuest(msg.packageName)
|
||||||
}
|
}
|
||||||
is TouchEvent -> Log.d(TAG, "Touch: ${msg.actions.size}")
|
is TouchEvent -> Log.d(TAG, "Touch: ${msg.actions.size}")
|
||||||
is VmKeyEvent -> Log.d(TAG, "Key: ${msg.keyCode}")
|
is VmKeyEvent -> Log.d(TAG, "Key: ${msg.keyCode}")
|
||||||
is SetAudioMode -> Log.d(TAG, "SetAudioMode: ${msg.mode}")
|
is SetAudioMode -> Log.d(TAG, "SetAudioMode: ${msg.mode}")
|
||||||
is Shutdown -> {
|
is Shutdown -> {
|
||||||
Log.i(TAG, "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))
|
is Ping -> server?.send(connId, VmProtocol.encode(Pong))
|
||||||
else -> Log.d(TAG, "Unhandled: $msg")
|
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 {
|
companion object {
|
||||||
const val WS_PORT = 8080
|
const val WS_PORT = 8080
|
||||||
const val WS_PATH = "/vm"
|
const val WS_PATH = "/vm"
|
||||||
|
|||||||
+78
-11
@@ -26,7 +26,7 @@ class VmGlassesController {
|
|||||||
|
|
||||||
private val testScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
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()
|
val state: StateFlow<GlassesScreenState> = _state.asStateFlow()
|
||||||
|
|
||||||
private val _frameSource = MutableStateFlow<FrameSource?>(null)
|
private val _frameSource = MutableStateFlow<FrameSource?>(null)
|
||||||
@@ -40,8 +40,7 @@ class VmGlassesController {
|
|||||||
Log.w(TAG, "acquireFrameSource: appContext not set")
|
Log.w(TAG, "acquireFrameSource: appContext not set")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
activeSource?.release()
|
releaseFrameSource()
|
||||||
activeSource = null
|
|
||||||
val surfaceTexture = try {
|
val surfaceTexture = try {
|
||||||
SurfaceTexture(true).also {
|
SurfaceTexture(true).also {
|
||||||
it.setDefaultBufferSize(VIRTUAL_WIDTH, VIRTUAL_HEIGHT)
|
it.setDefaultBufferSize(VIRTUAL_WIDTH, VIRTUAL_HEIGHT)
|
||||||
@@ -50,18 +49,39 @@ class VmGlassesController {
|
|||||||
Log.e(TAG, "SurfaceTexture creation failed", t)
|
Log.e(TAG, "SurfaceTexture creation failed", t)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val source = VirtualDisplayFrameSource.acquire(
|
val source = try {
|
||||||
context = ctx,
|
VirtualDisplayFrameSource.acquire(
|
||||||
surfaceTexture = surfaceTexture,
|
context = ctx,
|
||||||
serviceClass = VmShizukuService::class.java,
|
surfaceTexture = surfaceTexture,
|
||||||
width = VIRTUAL_WIDTH,
|
serviceClass = VmShizukuService::class.java,
|
||||||
height = VIRTUAL_HEIGHT,
|
width = VIRTUAL_WIDTH,
|
||||||
density = VIRTUAL_DENSITY,
|
height = VIRTUAL_HEIGHT,
|
||||||
)
|
density = VIRTUAL_DENSITY,
|
||||||
|
)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(TAG, "VirtualDisplayFrameSource.acquire failed for $packageName", t)
|
||||||
|
return null
|
||||||
|
}
|
||||||
activeSource = source
|
activeSource = source
|
||||||
|
_frameSource.value = source
|
||||||
return 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) {
|
fun setState(newState: GlassesScreenState) {
|
||||||
Log.i(TAG, "setState: $newState")
|
Log.i(TAG, "setState: $newState")
|
||||||
_state.value = 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) {
|
fun setAppContext(context: Context) {
|
||||||
this.appContext = context.applicationContext
|
this.appContext = context.applicationContext
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user