mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
new supported apps, redesign UI for apps not fully supported
This commit is contained in:
1 parent
4e4a246a51
commit
3994c9bd2e
6 files changed
+81
-14
No files matched your search
@@ -45,7 +45,19 @@ class ScreenProjector(private val context: Context) {
|
||||
/** Tracks the synthetic gesture so injected MotionEvents have a coherent downTime. */
|
||||
private var downTime = 0L
|
||||
|
||||
fun start(surface: Surface, width: Int, height: Int, densityDpi: Int, content: @Composable () -> Unit) {
|
||||
/**
|
||||
* [name] names the virtual display so the two callers are told apart from outside the app
|
||||
* (`dumpsys display`): the real car session keeps "gearslip-projection", the on-phone bench
|
||||
* preview passes "gearslip-preview".
|
||||
*/
|
||||
fun start(
|
||||
surface: Surface,
|
||||
width: Int,
|
||||
height: Int,
|
||||
densityDpi: Int,
|
||||
name: String = "gearslip-projection",
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
stop()
|
||||
|
||||
val displayManager = context.getSystemService(DisplayManager::class.java)
|
||||
@@ -57,7 +69,7 @@ class ScreenProjector(private val context: Context) {
|
||||
// driver on every connect.
|
||||
val display = try {
|
||||
displayManager.createVirtualDisplay(
|
||||
"gearslip-projection", width, height, densityDpi, surface,
|
||||
name, width, height, densityDpi, surface,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION or
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY,
|
||||
)
|
||||
|
||||
@@ -133,10 +133,16 @@ internal object LauncherCache {
|
||||
// own Messages screen does the same job, so a messenger gets only that tile.
|
||||
val messagingPackages = messagingApps.map { it.packageName }.toSet()
|
||||
|
||||
val nav = navApps.filter { it.component.packageName !in messagingPackages }.map {
|
||||
val label = if (it.component.packageName in mediaPackages) "${it.label} · Browse" else it.label
|
||||
Entry(label, iconOf(it.component.packageName), template = it)
|
||||
}
|
||||
val nav = navApps
|
||||
.filter { it.component.packageName !in messagingPackages }
|
||||
// An app whose template screen is rejected but whose player works through Gearslip
|
||||
// (Spotify) shows only its player tile - a "· Browse" tile beside it would just report
|
||||
// that the car screen isn't supported.
|
||||
.filter { !(KnownApps.screenBroken(it.component.packageName) && KnownApps.playerWorks(it.component.packageName)) }
|
||||
.map {
|
||||
val label = if (it.component.packageName in mediaPackages) "${it.label} · Browse" else it.label
|
||||
Entry(label, iconOf(it.component.packageName), template = it)
|
||||
}
|
||||
val media = mediaApps.map { Entry(it.label, iconOf(it.component.packageName), media = it) }
|
||||
// A messenger that is also a car media app keeps only its player tile.
|
||||
val messaging = messagingApps
|
||||
|
||||
@@ -111,6 +111,12 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
|
||||
}
|
||||
},
|
||||
)
|
||||
CarMedia.Rejection.NEEDS_APP_RUNNING -> Notice(
|
||||
"${app.label} isn't running.",
|
||||
"${app.label} plays through Gearslip's own player, but only while it's " +
|
||||
"open on the phone. Open it once and it keeps playing here in the car.",
|
||||
action = "Open ${app.label}" to { media.launchAndConnect(app) },
|
||||
)
|
||||
CarMedia.Rejection.REFUSED -> Notice(
|
||||
"${app.label} would not let Gearslip browse it.",
|
||||
"Some media apps only accept Google's own host.",
|
||||
|
||||
@@ -16,6 +16,8 @@ object KnownApps {
|
||||
"in.krosbits.musicolet", // Musicolet: browse, play, seek, lyrics
|
||||
"com.kododake.aabrowser", // Aardvark: browser template, starts, draws, takes touches
|
||||
"com.vivi.vivimusic", // Vivi Music: browse, play, seek, works well
|
||||
"com.mapquest.android.ace", // MapQuest: map, search, routing - works well
|
||||
"nl.flitsmeister", // Flitsmeister: map, routing, alerts - works well
|
||||
)
|
||||
|
||||
fun works(packageName: String): Boolean = packageName in working
|
||||
@@ -31,8 +33,10 @@ object KnownApps {
|
||||
fun playerWorks(packageName: String): Boolean = packageName in workingPlayer
|
||||
|
||||
/**
|
||||
* Apps whose car screen refuses Gearslip but whose player works. Only the "· Browse" tile
|
||||
* gets the red X. Car library 1.9 and later accepts only Google's host on a phone.
|
||||
* Apps whose car screen refuses Gearslip but whose player works. When the player covers the
|
||||
* app (see [playerWorks]) the launcher drops the dead "· Browse" tile entirely rather than
|
||||
* show it; otherwise that tile gets a red X. Car library 1.9 and later accepts only Google's
|
||||
* host on a phone.
|
||||
*/
|
||||
private val brokenScreen = setOf(
|
||||
"com.spotify.music", // Spotify 9.1.86 and later: car library 1.9 rejects Gearslip
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.seb3thehacker.gearslip.media
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadata
|
||||
import android.media.Rating
|
||||
@@ -19,6 +20,7 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import app.seb3thehacker.gearslip.GearslipLog
|
||||
import app.seb3thehacker.gearslip.host.KnownApps
|
||||
import app.seb3thehacker.gearslip.notify.GearslipNotificationListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -142,7 +144,7 @@ class CarMedia(private val context: Context) {
|
||||
enum class Phase { IDLE, CONNECTING, READY, REJECTED }
|
||||
|
||||
/** Why a connection was [Phase.REJECTED], so the screen can say something useful. */
|
||||
enum class Rejection { REFUSED, NEEDS_NOTIFICATION_ACCESS }
|
||||
enum class Rejection { REFUSED, NEEDS_NOTIFICATION_ACCESS, NEEDS_APP_RUNNING }
|
||||
|
||||
private val _rejection = MutableStateFlow(Rejection.REFUSED)
|
||||
val rejection: StateFlow<Rejection> = _rejection.asStateFlow()
|
||||
@@ -236,7 +238,7 @@ class CarMedia(private val context: Context) {
|
||||
* is out of reach. Reading another app's session needs Notification access, which the driver
|
||||
* grants once on the phone.
|
||||
*/
|
||||
private fun attachToSession(app: MediaApp, attempt: Int = 0) {
|
||||
private fun attachToSession(app: MediaApp, attempt: Int = 0, launched: Boolean = false) {
|
||||
if (connecting != app) return
|
||||
val manager = context.getSystemService(MediaSessionManager::class.java)
|
||||
val listener = ComponentName(context, GearslipNotificationListener::class.java)
|
||||
@@ -249,12 +251,19 @@ class CarMedia(private val context: Context) {
|
||||
return
|
||||
}
|
||||
if (session == null) {
|
||||
val limit = if (launched) SESSION_RETRIES_LAUNCHED else SESSION_RETRIES
|
||||
// The app only publishes its session once its service has started; give it a moment.
|
||||
if (attempt < SESSION_RETRIES) {
|
||||
main.postDelayed({ attachToSession(app, attempt + 1) }, SESSION_RETRY_MS)
|
||||
if (attempt < limit) {
|
||||
main.postDelayed({ attachToSession(app, attempt + 1, launched) }, SESSION_RETRY_MS)
|
||||
} else {
|
||||
GearslipLog.w("media: ${app.label} has no active session to control")
|
||||
_rejection.value = Rejection.REFUSED
|
||||
// A session-only app (Spotify) just isn't running - say so and offer to open it,
|
||||
// rather than calling it unsupported.
|
||||
_rejection.value = if (KnownApps.playerWorks(app.component.packageName)) {
|
||||
Rejection.NEEDS_APP_RUNNING
|
||||
} else {
|
||||
Rejection.REFUSED
|
||||
}
|
||||
_phase.value = Phase.REJECTED
|
||||
}
|
||||
return
|
||||
@@ -437,8 +446,38 @@ class CarMedia(private val context: Context) {
|
||||
_queue.value = emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens [app] on the phone, then connects once it publishes its session. The driver asks for
|
||||
* this from the "Open" button a session-only app (Spotify) shows when it isn't running yet.
|
||||
*/
|
||||
fun launchAndConnect(app: MediaApp) {
|
||||
if (!openApp(app)) return
|
||||
disconnect()
|
||||
connecting = app
|
||||
_phase.value = Phase.CONNECTING
|
||||
// A cold start takes a few seconds to publish a session, so wait, then watch for longer.
|
||||
main.postDelayed({ attachToSession(app, launched = true) }, LAUNCH_WAIT_MS)
|
||||
}
|
||||
|
||||
/** Brings [app] to the foreground on the phone so it publishes its session. */
|
||||
private fun openApp(app: MediaApp): Boolean {
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(app.component.packageName) ?: return false
|
||||
return runCatching {
|
||||
context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
GearslipLog.i("media: opened ${app.label} to bring up its session")
|
||||
true
|
||||
}.getOrElse {
|
||||
GearslipLog.w("media: could not open ${app.label}: ${it.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SESSION_RETRIES = 6
|
||||
const val SESSION_RETRY_MS = 500L
|
||||
/** After opening a cold app, how long to wait before the first look for its session. */
|
||||
const val LAUNCH_WAIT_MS = 1_500L
|
||||
/** Retries once the app has been opened: a cold start can take a few seconds to publish. */
|
||||
const val SESSION_RETRIES_LAUNCHED = 16
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ fun CarPreviewScreen(onBack: () -> Unit) {
|
||||
}
|
||||
}, handler)
|
||||
|
||||
projector.start(reader.surface, width, height, density) { CarUi() }
|
||||
projector.start(reader.surface, width, height, density, name = "gearslip-preview") { CarUi() }
|
||||
|
||||
onDispose {
|
||||
projector.stop()
|
||||
|
||||
Reference in new issue
Block a user