Compare commits

...
6 changed files with 259 additions and 14 deletions

No files matched your search

@@ -346,12 +346,20 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private var secondScreenDisplays by mutableStateOf<List<SecondScreenDisplay>>(emptyList())
private var selectedSecondScreenDisplay by mutableStateOf(SessionState.secondScreenDisplay)
private var started = false
private var sessionActivityDisplayId = Display.DEFAULT_DISPLAY
private lateinit var displayManager: DisplayManager
private var secondScreenPresentation: SecondScreenWindow? = null
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = refreshSecondScreenDisplays()
override fun onDisplayRemoved(displayId: Int) = refreshSecondScreenDisplays()
override fun onDisplayAdded(displayId: Int) {
notifyActivityDisplayStateChanged(displayId)
refreshSecondScreenDisplays()
}
override fun onDisplayRemoved(displayId: Int) {
notifyActivityDisplayStateChanged(displayId)
refreshSecondScreenDisplays()
}
override fun onDisplayChanged(displayId: Int) {
notifyActivityDisplayStateChanged(displayId)
followPanelRefresh(displayId)
refreshSecondScreenDisplays()
}
@@ -1522,6 +1530,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
intent.getStringExtra(SessionService.EXTRA_STEAM_UI),
intent.getStringExtra(SessionService.EXTRA_STEAM_URL),
intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS),
activityDisplayId = sessionActivityDisplayId,
)
}
applyFrameGen()
@@ -1571,6 +1580,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
super.onConfigurationChanged(newConfig)
if (refreshActivityDisplayId() && started) {
SessionService.activityDisplayChanged(this, sessionActivityDisplayId)
}
applyFollowOrientation(newConfig)
applyUnfoldedControls()
}
@@ -2118,7 +2130,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private fun refreshSecondScreenDisplays() {
if (!::displayManager.isInitialized) return
val primaryId = display?.displayId ?: windowManager.defaultDisplay.displayId
val primaryId = windowManager.defaultDisplay.displayId
val candidates = SecondScreenDisplays.available(displayManager, primaryId)
.filter { glassesScreen == null || it.id != glasses.displayId }
val oldIds = secondScreenDisplays.map { it.id }.toSet()
@@ -2133,6 +2145,24 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
else if (started && secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode)
}
@Suppress("DEPRECATION")
private fun currentActivityDisplayId(): Int = windowManager.defaultDisplay.displayId
private fun refreshActivityDisplayId(): Boolean {
val current = currentActivityDisplayId()
val changed = sessionActivityDisplayId != current
sessionActivityDisplayId = current
SessionState.activityDisplayId = current
return changed
}
private fun notifyActivityDisplayStateChanged(changedDisplayId: Int) {
val changed = refreshActivityDisplayId()
if (started && (changed || changedDisplayId == sessionActivityDisplayId)) {
SessionService.activityDisplayChanged(this, sessionActivityDisplayId)
}
}
private fun selectSecondScreenDisplay(displayId: Int) {
if (secondScreenDisplays.none { it.id == displayId }) return
selectedSecondScreenDisplay = displayId
@@ -2624,11 +2654,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
super.onStart()
if (runtimeRemovalBlocked) return
started = true
refreshActivityDisplayId()
displayManager.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
refreshSecondScreenDisplays()
if (secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode)
startedScreens++
SessionService.setActivityVisible(this, true)
SessionService.setActivityVisible(this, true, sessionActivityDisplayId)
Glasses.rescan()
updateGlassesScreen()
updateHandCamera()
@@ -2642,7 +2673,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
closeSecondScreen(reset = false)
startedScreens = (startedScreens - 1).coerceAtLeast(0)
updateGlassesScreen()
if (startedScreens == 0) SessionService.setActivityVisible(this, false)
if (startedScreens == 0) SessionService.setActivityVisible(this, false, sessionActivityDisplayId)
super.onStop()
}
@@ -2673,6 +2704,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
override fun onResume() {
super.onResume()
if (refreshActivityDisplayId() && started) {
SessionService.activityDisplayChanged(this, sessionActivityDisplayId)
}
applyFollowOrientation(resources.configuration)
if (runtimeRemovalBlocked) return
com.droiddeck.launcher.ui.Motion.refresh(this)
@@ -13,6 +13,7 @@ import android.content.BroadcastReceiver
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.hardware.display.DisplayManager
import android.net.TrafficStats
import android.net.wifi.WifiManager
import android.os.Build
@@ -22,6 +23,7 @@ import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.util.Log
import android.view.Display
import com.droiddeck.launcher.R
import com.droiddeck.launcher.SessionActivity
import com.droiddeck.launcher.audio.DirectAudioRelayComponent
@@ -71,10 +73,11 @@ class SessionService : Service() {
private var suspendPolicy = SessionPrefs.SUSPEND_MANUAL
private var steamDownloadsInBackground = false
private var activityVisible = true
private var activityDisplayId = Display.DEFAULT_DISPLAY
private var xrActivityVisible = false
private var signInHold = false
private val hasVisibleSurface get() = sessionHasVisibleSurface(activityVisible || signInHold, screenOn, xrActivityVisible)
private var screenOn = true
private var previouslyHidden: Boolean? = null
private var manualPauseRequested = false
private var steamSleepToken: String? = null
private var nativeSleepToken: String? = null
@@ -97,7 +100,6 @@ class SessionService : Service() {
Intent.ACTION_SCREEN_ON -> screenOn = true
else -> return
}
if (screenOn) resumeSteamSleepOnReturn()
suspendAttemptFailed = false
updateSuspendPolicy()
}
@@ -159,15 +161,16 @@ class SessionService : Service() {
return START_NOT_STICKY
}
ACTION_ACTIVITY_VISIBLE -> {
activityDisplayId = intent.getIntExtra(EXTRA_ACTIVITY_DISPLAY_ID, activityDisplayId)
if (!SessionState.pipActive) pipTask = false
activityVisible = true
suspendDownloadNow = false
resumeSteamSleepOnReturn()
suspendAttemptFailed = false
updateSuspendPolicy()
return START_NOT_STICKY
}
ACTION_ACTIVITY_HIDDEN -> {
activityDisplayId = intent.getIntExtra(EXTRA_ACTIVITY_DISPLAY_ID, activityDisplayId)
activityVisible = false
suspendAttemptFailed = false
updateSuspendPolicy()
@@ -184,6 +187,12 @@ class SessionService : Service() {
updateSuspendPolicy()
return START_NOT_STICKY
}
ACTION_ACTIVITY_DISPLAY_CHANGED -> {
activityDisplayId = intent.getIntExtra(EXTRA_ACTIVITY_DISPLAY_ID, activityDisplayId)
suspendAttemptFailed = false
updateSuspendPolicy()
return START_NOT_STICKY
}
ACTION_SIGN_IN_HOLD -> {
val hold = intent?.getBooleanExtra(EXTRA_HOLD, false) == true
if (hold == signInHold) return START_NOT_STICKY
@@ -228,9 +237,11 @@ class SessionService : Service() {
suspendPolicy = SessionPrefs.suspendPolicy(this, SessionState.mode)
steamDownloadsInBackground = SessionPrefs.steamDownloadsInBackground(this)
activityVisible = true
activityDisplayId = SessionState.activityDisplayId
xrActivityVisible = false
signInHold = false
screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: true
previouslyHidden = null
manualPauseRequested = false
suspendDownloadNow = false
steamDownloadMonitor.reset()
@@ -1211,11 +1222,17 @@ class SessionService : Service() {
private fun updateSuspendPolicy() {
if (!SessionState.running) return
if (suspendPolicy == SessionPrefs.SUSPEND_MANUAL && !hasVisibleSurface) {
val activityDisplayOn = activityDisplayOn()
val hidden = surfaceHidden()
val wasHidden = previouslyHidden
previouslyHidden = hidden
if (SessionVisibilityPolicy.shouldResumeOnVisibleTransition(wasHidden, hidden, suspendPolicy)) {
resumeSteamSleepOnReturn()
}
if (suspendPolicy == SessionPrefs.SUSPEND_MANUAL && hidden) {
manualPauseRequested = true
}
val controller = suspendController ?: return
val hidden = !hasVisibleSurface
val downloadsBlockingSuspend = steamDownloadsInBackground && SessionState.mode == MODE_STEAM &&
suspendPolicy != SessionPrefs.SUSPEND_NEVER && hidden && downloadActive && !suspendDownloadNow
if (downloadsBlockingSuspend) acquireLocks()
@@ -1236,7 +1253,9 @@ class SessionService : Service() {
suspendOperationPending = true
SessionEvents.record(if (shouldSuspend) "session.suspend_requested" else "session.resume_requested", mapOf(
"policy" to suspendPolicy, "activityVisible" to activityVisible,
"screenOn" to screenOn, "steamSleep" to (steamSleepToken != null),
"screenOn" to screenOn, "activityDisplayId" to activityDisplayId,
"activityDisplayOn" to activityDisplayOn, "activityHidden" to hidden,
"steamSleep" to (steamSleepToken != null),
"xrActivityVisible" to xrActivityVisible,
"nativeSleep" to (nativeSleepToken != null),
))
@@ -1295,13 +1314,27 @@ class SessionService : Service() {
}
private fun resumeSteamSleepOnReturn() {
if (suspendPolicy in setOf(SessionPrefs.SUSPEND_AUTO, SessionPrefs.SUSPEND_NATIVE) && hasVisibleSurface) {
if (suspendPolicy in setOf(SessionPrefs.SUSPEND_AUTO, SessionPrefs.SUSPEND_NATIVE) && !surfaceHidden()) {
if (steamSleepToken == null && (nativeSleepToken == null || nativeSleepFallback)) requestSteamWake()
completeNativeSleep()
completeSteamSleep()
}
}
private fun surfaceHidden() = SessionVisibilityPolicy.isHidden(
activityStarted = activityVisible || signInHold,
screenOn = screenOn,
activityOnDefaultDisplay = activityDisplayId == Display.DEFAULT_DISPLAY,
activityDisplayOn = activityDisplayOn(),
xrActivityVisible = xrActivityVisible,
)
private fun activityDisplayOn(): Boolean {
if (activityDisplayId == Display.DEFAULT_DISPLAY) return screenOn
val displays = getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager ?: return false
return displays.getDisplay(activityDisplayId)?.state == Display.STATE_ON
}
private fun prepareNativeSleep() {
val token = UUID.randomUUID().toString().replace("-", "")
nativeSleepToken = token
@@ -1684,6 +1717,7 @@ class SessionService : Service() {
private const val ACTION_SUSPEND_POLICY_CHANGED = "com.droiddeck.launcher.SUSPEND_POLICY_CHANGED"
private const val ACTION_ACTIVITY_VISIBLE = "com.droiddeck.launcher.ACTIVITY_VISIBLE"
private const val ACTION_ACTIVITY_HIDDEN = "com.droiddeck.launcher.ACTIVITY_HIDDEN"
private const val ACTION_ACTIVITY_DISPLAY_CHANGED = "com.droiddeck.launcher.ACTIVITY_DISPLAY_CHANGED"
private const val ACTION_XR_ACTIVITY_VISIBLE = "com.droiddeck.launcher.XR_ACTIVITY_VISIBLE"
private const val ACTION_XR_ACTIVITY_HIDDEN = "com.droiddeck.launcher.XR_ACTIVITY_HIDDEN"
private const val ACTION_SIGN_IN_HOLD = "com.droiddeck.launcher.SIGN_IN_HOLD"
@@ -1716,11 +1750,14 @@ class SessionService : Service() {
const val EXTRA_STEAM_UI = "steamUi"
const val EXTRA_STEAM_URL = "steamUrl"
const val EXTRA_GAME_ID = "gameId"
private const val EXTRA_ACTIVITY_DISPLAY_ID = "activityDisplayId"
fun start(
context: Context, mode: String = MODE_STEAM, program: String? = null,
steamUi: String? = null, steamUrl: String? = null, programArgs: Array<String>? = null,
activityDisplayId: Int = Display.DEFAULT_DISPLAY,
) {
SessionState.activityDisplayId = activityDisplayId
val intent = Intent(context, SessionService::class.java).putExtra(EXTRA_MODE, mode)
if (program != null) intent.putExtra(EXTRA_PROGRAM, program)
if (programArgs != null) intent.putExtra(EXTRA_PROGRAM_ARGS, programArgs)
@@ -1742,10 +1779,31 @@ class SessionService : Service() {
context.startService(Intent(context, SessionService::class.java).setAction(ACTION_PIP_BEGIN))
}
fun setActivityVisible(context: Context, visible: Boolean) {
fun setActivityVisible(
context: Context,
visible: Boolean,
activityDisplayId: Int = currentActivityDisplayId(context),
) {
SessionState.activityDisplayId = activityDisplayId
if (!SessionState.running) return
val action = if (visible) ACTION_ACTIVITY_VISIBLE else ACTION_ACTIVITY_HIDDEN
context.startService(Intent(context, SessionService::class.java).setAction(action))
context.startService(Intent(context, SessionService::class.java)
.setAction(action)
.putExtra(EXTRA_ACTIVITY_DISPLAY_ID, activityDisplayId))
}
fun activityDisplayChanged(context: Context, activityDisplayId: Int) {
SessionState.activityDisplayId = activityDisplayId
if (!SessionState.running) return
context.startService(Intent(context, SessionService::class.java)
.setAction(ACTION_ACTIVITY_DISPLAY_CHANGED)
.putExtra(EXTRA_ACTIVITY_DISPLAY_ID, activityDisplayId))
}
@Suppress("DEPRECATION")
private fun currentActivityDisplayId(context: Context): Int {
val activity = context as? android.app.Activity ?: return Display.DEFAULT_DISPLAY
return activity.windowManager.defaultDisplay.displayId
}
fun setXrActivityVisible(context: Context, visible: Boolean) {
@@ -42,6 +42,7 @@ object SessionState {
@Volatile
var running = false
@Volatile
var activityDisplayId = 0
var runtimeDriver: String? = null
var suspended by mutableStateOf(false)
@Volatile var pipActive = false
@@ -0,0 +1,22 @@
package com.droiddeck.launcher.session
internal object SessionVisibilityPolicy {
fun isHidden(
activityStarted: Boolean,
screenOn: Boolean,
activityOnDefaultDisplay: Boolean,
activityDisplayOn: Boolean,
xrActivityVisible: Boolean = false,
): Boolean = !sessionHasVisibleSurface(
panelVisible = activityStarted,
screenOn = if (activityOnDefaultDisplay) screenOn else activityDisplayOn,
xrVisible = xrActivityVisible,
)
fun shouldResumeOnVisibleTransition(
previouslyHidden: Boolean?,
currentlyHidden: Boolean,
suspendPolicy: String,
): Boolean = previouslyHidden == true && !currentlyHidden &&
suspendPolicy in setOf(SessionPrefs.SUSPEND_AUTO, SessionPrefs.SUSPEND_NATIVE)
}
@@ -0,0 +1,29 @@
package com.droiddeck.launcher.session
import android.app.Activity
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [26, 28, 29, 33], manifest = Config.NONE)
class ActivityDisplayCompatibilityTest {
@Test fun visibleActivityUsesItsDisplayIdAcrossSupportedAndroidVersions() {
val wasRunning = SessionState.running
val previousDisplayId = SessionState.activityDisplayId
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
try {
SessionState.running = false
SessionService.setActivityVisible(activity, true)
assertEquals(activity.windowManager.defaultDisplay.displayId, SessionState.activityDisplayId)
} finally {
SessionState.running = wasRunning
SessionState.activityDisplayId = previousDisplayId
activity.finish()
}
}
}
@@ -0,0 +1,101 @@
package com.droiddeck.launcher.session
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class SessionVisibilityPolicyTest {
@Test fun handsetScreenLockHidesActivityOnDefaultDisplay() {
assertFalse(SessionVisibilityPolicy.isHidden(true, true, true, true))
assertTrue(SessionVisibilityPolicy.isHidden(true, false, true, true))
}
@Test fun handsetScreenOffDoesNotHideActivityOnPoweredOnDexDisplay() {
assertFalse(SessionVisibilityPolicy.isHidden(true, false, false, true))
}
@Test fun externalDisplayOffHidesActivityEvenWhenHandsetIsOn() {
assertTrue(SessionVisibilityPolicy.isHidden(true, true, false, false))
}
@Test fun disconnectedExternalDisplayIsHidden() {
assertTrue(SessionVisibilityPolicy.isHidden(true, false, false, false))
}
@Test fun hiddenExternalActivityIsHiddenWhileItsDisplayIsOn() {
assertTrue(SessionVisibilityPolicy.isHidden(false, false, false, true))
}
@Test fun startedButUnfocusedMultiwindowActivityRemainsVisible() {
assertFalse(SessionVisibilityPolicy.isHidden(true, true, true, true))
}
@Test fun defaultDisplayScreenOnResumesAfterScreenOff() {
val wasHidden = SessionVisibilityPolicy.isHidden(true, false, true, true)
val isHidden = SessionVisibilityPolicy.isHidden(true, true, true, true)
assertTrue(SessionVisibilityPolicy.shouldResumeOnVisibleTransition(
previouslyHidden = wasHidden,
currentlyHidden = isHidden,
suspendPolicy = SessionPrefs.SUSPEND_AUTO,
))
}
@Test fun externalDisplayOnResumesAfterOffOrDisconnect() {
val wasHidden = SessionVisibilityPolicy.isHidden(true, false, false, false)
val isHidden = SessionVisibilityPolicy.isHidden(true, false, false, true)
assertTrue(SessionVisibilityPolicy.shouldResumeOnVisibleTransition(
previouslyHidden = wasHidden,
currentlyHidden = isHidden,
suspendPolicy = SessionPrefs.SUSPEND_NATIVE,
))
}
@Test fun startedActivityResumesAfterBeingStopped() {
val wasHidden = SessionVisibilityPolicy.isHidden(false, false, false, true)
val isHidden = SessionVisibilityPolicy.isHidden(true, false, false, true)
assertTrue(SessionVisibilityPolicy.shouldResumeOnVisibleTransition(
previouslyHidden = wasHidden,
currentlyHidden = isHidden,
suspendPolicy = SessionPrefs.SUSPEND_AUTO,
))
}
@Test fun onToOnDisplayPropertyChangeDoesNotResume() {
val wasHidden = SessionVisibilityPolicy.isHidden(true, true, false, true)
val isHidden = SessionVisibilityPolicy.isHidden(true, true, false, true)
assertFalse(SessionVisibilityPolicy.shouldResumeOnVisibleTransition(
previouslyHidden = wasHidden,
currentlyHidden = isHidden,
suspendPolicy = SessionPrefs.SUSPEND_AUTO,
))
assertFalse(SessionVisibilityPolicy.shouldResumeOnVisibleTransition(
previouslyHidden = null,
currentlyHidden = isHidden,
suspendPolicy = SessionPrefs.SUSPEND_AUTO,
))
}
@Test fun handsetScreenOnDoesNotResumeVisibleExternalDisplay() {
val wasHidden = SessionVisibilityPolicy.isHidden(true, false, false, true)
val isHidden = SessionVisibilityPolicy.isHidden(true, true, false, true)
assertFalse(SessionVisibilityPolicy.shouldResumeOnVisibleTransition(
previouslyHidden = wasHidden,
currentlyHidden = isHidden,
suspendPolicy = SessionPrefs.SUSPEND_AUTO,
))
}
@Test fun xrGlassesKeepTheSessionVisibleWithTheHandsetScreenOff() {
assertFalse(SessionVisibilityPolicy.isHidden(false, false, true, false, xrActivityVisible = true))
}
@Test fun manualPolicyDoesNotResumeOnVisibleTransition() {
val wasHidden = SessionVisibilityPolicy.isHidden(true, false, true, true)
val isHidden = SessionVisibilityPolicy.isHidden(true, true, true, true)
assertFalse(SessionVisibilityPolicy.shouldResumeOnVisibleTransition(
previouslyHidden = wasHidden,
currentlyHidden = isHidden,
suspendPolicy = SessionPrefs.SUSPEND_MANUAL,
))
}
}