Compare commits

...
6 changed files with 166 additions and 11 deletions

No files matched your search

@@ -1601,6 +1601,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
intent.getStringExtra(SessionService.EXTRA_STEAM_UI),
intent.getStringExtra(SessionService.EXTRA_STEAM_URL),
intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS),
shownOnDisplayId,
)
}
applyFrameGen()
@@ -1651,6 +1652,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
super.onConfigurationChanged(newConfig)
if (started) SessionService.setActivityDisplay(this, shownOnDisplayId)
cancelTouchGestures()
applyFollowOrientation(newConfig)
applyUnfoldedControls()
@@ -2274,9 +2276,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
return true
}
private val shownOnDisplayId: Int get() =
if (Build.VERSION.SDK_INT >= 30) display?.displayId ?: windowManager.defaultDisplay.displayId
else windowManager.defaultDisplay.displayId
private fun refreshSecondScreenDisplays() {
if (!::displayManager.isInitialized) return
val primaryId = display?.displayId ?: windowManager.defaultDisplay.displayId
val primaryId = shownOnDisplayId
val candidates = SecondScreenDisplays.available(displayManager, primaryId)
.filter { glassesScreen == null || it.id != glasses.displayId }
val oldIds = secondScreenDisplays.map { it.id }.toSet()
@@ -2799,7 +2805,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
refreshSecondScreenDisplays()
if (secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode)
startedScreens++
SessionService.setActivityVisible(this, true)
SessionService.setActivityVisible(this, true, shownOnDisplayId)
Glasses.rescan()
updateGlassesScreen()
updateHandCamera()
@@ -22,6 +22,8 @@ import android.os.IBinder
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.hardware.display.DisplayManager
import android.view.Display
import android.util.Log
import com.droiddeck.launcher.R
import com.droiddeck.launcher.SessionActivity
@@ -76,6 +78,7 @@ class SessionService : Service() {
private var signInHold = false
private val hasVisibleSurface get() = sessionHasVisibleSurface(activityVisible || signInHold, screenOn, xrActivityVisible)
private var screenOn = true
private var sessionDisplayId = Display.DEFAULT_DISPLAY
private var manualPauseRequested = false
private var steamSleepToken: String? = null
private var nativeSleepToken: String? = null
@@ -95,8 +98,8 @@ class SessionService : Service() {
private val screenReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> screenOn = false
Intent.ACTION_SCREEN_ON -> screenOn = true
Intent.ACTION_SCREEN_OFF -> screenOn = readScreenOn(false)
Intent.ACTION_SCREEN_ON -> screenOn = readScreenOn(true)
else -> return
}
if (screenOn) resumeSteamSleepOnReturn()
@@ -104,17 +107,49 @@ class SessionService : Service() {
updateSuspendPolicy()
}
}
private var displayListenerRegistered = false
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayRemoved(displayId: Int) {
if (displayId != sessionDisplayId) return
sessionDisplayId = Display.DEFAULT_DISPLAY
refreshScreenOn()
}
override fun onDisplayChanged(displayId: Int) {
if (displayId == sessionDisplayId && displayId != Display.DEFAULT_DISPLAY) refreshScreenOn()
}
}
private fun readScreenOn(interactive: Boolean? = null): Boolean {
val id = sessionDisplayId
val state = if (id == Display.DEFAULT_DISPLAY) null
else (getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager)?.getDisplay(id)?.state
return sessionScreenOn(id == Display.DEFAULT_DISPLAY,
interactive ?: (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: true, state)
}
private fun refreshScreenOn() {
val on = readScreenOn()
if (on == screenOn) return
screenOn = on
if (on) resumeSteamSleepOnReturn()
suspendAttemptFailed = false
updateSuspendPolicy()
}
@Volatile private var sessionGen = 0
override fun onCreate() {
super.onCreate()
val power = getSystemService(Context.POWER_SERVICE) as? PowerManager
screenOn = power?.isInteractive ?: true
screenOn = readScreenOn()
registerReceiver(screenReceiver, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
})
screenReceiverRegistered = true
(getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager)?.let {
it.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
displayListenerRegistered = true
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -163,12 +198,19 @@ class SessionService : Service() {
ACTION_ACTIVITY_VISIBLE -> {
if (!SessionState.pipActive) pipTask = false
activityVisible = true
sessionDisplayId = intent.getIntExtra(EXTRA_DISPLAY_ID, sessionDisplayId)
screenOn = readScreenOn()
suspendDownloadNow = false
resumeSteamSleepOnReturn()
suspendAttemptFailed = false
updateSuspendPolicy()
return START_NOT_STICKY
}
ACTION_ACTIVITY_DISPLAY -> {
sessionDisplayId = intent.getIntExtra(EXTRA_DISPLAY_ID, sessionDisplayId)
refreshScreenOn()
return START_NOT_STICKY
}
ACTION_ACTIVITY_HIDDEN -> {
activityVisible = false
suspendAttemptFailed = false
@@ -232,7 +274,8 @@ class SessionService : Service() {
activityVisible = true
xrActivityVisible = false
signInHold = false
screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: true
sessionDisplayId = intent?.getIntExtra(EXTRA_DISPLAY_ID, Display.DEFAULT_DISPLAY) ?: Display.DEFAULT_DISPLAY
screenOn = readScreenOn()
manualPauseRequested = false
suspendDownloadNow = false
steamDownloadMonitor.reset()
@@ -1022,7 +1065,7 @@ class SessionService : Service() {
private fun resumeSession(panelVisible: Boolean = true) {
if (panelVisible) activityVisible = true
screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: screenOn
screenOn = readScreenOn()
manualPauseRequested = false
suspendDownloadNow = false
if (steamSleepToken == null && (nativeSleepToken == null || nativeSleepFallback || nativeSleepIdle)) requestSteamWake()
@@ -1248,7 +1291,7 @@ 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, "display" to sessionDisplayId, "steamSleep" to (steamSleepToken != null),
"xrActivityVisible" to xrActivityVisible,
"nativeSleep" to (nativeSleepToken != null),
))
@@ -1512,6 +1555,10 @@ class SessionService : Service() {
unregisterReceiver(screenReceiver)
screenReceiverRegistered = false
}
if (displayListenerRegistered) {
(getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager)?.unregisterDisplayListener(displayListener)
displayListenerRegistered = false
}
super.onDestroy()
}
@@ -1699,6 +1746,8 @@ 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 = "com.droiddeck.launcher.ACTIVITY_DISPLAY"
private const val EXTRA_DISPLAY_ID = "displayId"
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"
@@ -1735,8 +1784,10 @@ class SessionService : Service() {
fun start(
context: Context, mode: String = MODE_STEAM, program: String? = null,
steamUi: String? = null, steamUrl: String? = null, programArgs: Array<String>? = null,
displayId: Int = Display.DEFAULT_DISPLAY,
) {
val intent = Intent(context, SessionService::class.java).putExtra(EXTRA_MODE, mode)
.putExtra(EXTRA_DISPLAY_ID, displayId)
if (program != null) intent.putExtra(EXTRA_PROGRAM, program)
if (programArgs != null) intent.putExtra(EXTRA_PROGRAM_ARGS, programArgs)
if (steamUi != null) intent.putExtra(EXTRA_STEAM_UI, steamUi)
@@ -1757,10 +1808,15 @@ 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, displayId: Int = Display.DEFAULT_DISPLAY) {
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_DISPLAY_ID, displayId))
}
fun setActivityDisplay(context: Context, displayId: Int) {
if (!SessionState.running) return
context.startService(Intent(context, SessionService::class.java).setAction(ACTION_ACTIVITY_DISPLAY).putExtra(EXTRA_DISPLAY_ID, displayId))
}
fun setXrActivityVisible(context: Context, visible: Boolean) {
@@ -1,4 +1,10 @@
package com.droiddeck.launcher.session
import android.view.Display
internal fun sessionHasVisibleSurface(panelVisible: Boolean, screenOn: Boolean, xrVisible: Boolean): Boolean =
xrVisible || (panelVisible && screenOn)
internal fun sessionScreenOn(onDefaultDisplay: Boolean, interactive: Boolean, displayState: Int?): Boolean =
if (onDefaultDisplay || displayState == null || displayState == Display.STATE_UNKNOWN) interactive
else displayState == Display.STATE_ON || displayState == Display.STATE_VR || displayState == Display.STATE_ON_SUSPEND
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.session
import android.view.Display
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -20,6 +21,34 @@ class SessionVisibilityTest {
assertFalse(sessionHasVisibleSurface(false, false, false))
}
@Test fun phoneDisplayFollowsTheInteractiveState() {
assertTrue(sessionScreenOn(true, true, null))
assertFalse(sessionScreenOn(true, false, null))
assertFalse(sessionScreenOn(true, false, Display.STATE_ON))
}
@Test fun externalDisplayKeepsTheSessionAwakeWhileThePhoneScreenIsOff() {
assertTrue(sessionScreenOn(false, false, Display.STATE_ON))
assertTrue(sessionScreenOn(false, false, Display.STATE_ON_SUSPEND))
assertTrue(sessionScreenOn(false, false, Display.STATE_VR))
}
@Test fun externalDisplayTurnedOffSuspendsEvenWithThePhoneScreenOn() {
assertFalse(sessionScreenOn(false, true, Display.STATE_OFF))
assertFalse(sessionScreenOn(false, true, Display.STATE_DOZE))
assertFalse(sessionScreenOn(false, true, Display.STATE_DOZE_SUSPEND))
}
@Test fun missingExternalDisplayFallsBackToThePhone() {
assertTrue(sessionScreenOn(false, true, null))
assertFalse(sessionScreenOn(false, false, null))
}
@Test fun unknownExternalDisplayStateFallsBackToThePhone() {
assertTrue(sessionScreenOn(false, true, Display.STATE_UNKNOWN))
assertFalse(sessionScreenOn(false, false, Display.STATE_UNKNOWN))
}
@Test fun losingXrVisibilityReturnsToNormalSuspendRules() {
assertTrue(sessionHasVisibleSurface(false, false, true))
assertFalse(sessionHasVisibleSurface(false, false, false))
@@ -271,6 +271,23 @@ def provision_prefix(compat_data, wait=SERVER_WAIT):
return state + (" server-running" if busy else "")
def create_prefix(compat_data, proton, timeout=120):
if not compat_data or not proton or not Path(proton).is_file():
return "no-proton"
reg = Path(compat_data) / "pfx/user.reg"
if reg.is_file():
return "present"
try:
Path(compat_data).mkdir(parents=True, exist_ok=True)
subprocess.run([proton, "run", "cmd.exe", "/d", "/c", "exit"], stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout)
except subprocess.TimeoutExpired:
return "timeout"
except OSError:
return "failed"
return "created" if reg.is_file() else "failed"
CLOUD_TIMEOUT = 15.0
CLOUD_STORES = ("gog", "epic")
STORE_GAME = 10
@@ -502,6 +519,10 @@ def main(argv):
compat_data = os.environ.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/")
prefix_id = os.path.basename(compat_data) if compat_data else ""
launch_dir = os.environ.get("BL_LAUNCH_DIR", "")
if launch_dir and cloud_on(sidecar):
made = create_prefix(compat_data, os.environ.get("BL_STORE_PROTON", ""))
if made != "present":
log("cloud %s %s prefix=%s" % (store, game, made))
if store == "gog":
prepare_gog(sidecar, compat_data, launch_dir)
log("gog registry: " + apply_gog_registry(folder, compat_data, os.environ.get("BL_STORE_PROTON", "")))
+37
View File
@@ -226,6 +226,43 @@ class CloudHooks(unittest.TestCase):
self.assertEqual(store.CLOUD_TIMEOUT, asked[1][1])
self.assertEqual('cloud-up', asked[2][0]['op'])
def test_a_first_launch_creates_the_prefix_before_the_cloud_download(self):
with tempfile.TemporaryDirectory() as root:
folder = game(root, store_id='epic', ident='fa4240e5')
sidecar = json.loads((folder / store.SIDECAR).read_text())
sidecar['epic'] = {'eos': False, 'offline': True}
(folder / store.SIDECAR).write_text(json.dumps(sidecar))
proton = Path(root) / 'proton'
proton.write_text('selected Proton')
compat = Path(root) / 'compatdata/1'
order = []
def run(argv, **kwargs):
order.append(argv)
(compat / 'pfx').mkdir(parents=True)
(compat / 'pfx/user.reg').write_text('WINE REGISTRY Version 2\n')
return __import__('subprocess').CompletedProcess(argv, 0)
env = {'BL_LAUNCH_DIR': root, 'STEAM_COMPAT_DATA_PATH': str(compat), 'BL_STORE_PROTON': str(proton)}
with patch.dict(os.environ, env), patch.object(store.subprocess, 'run', side_effect=run), \
patch.object(store, 'ask', side_effect=lambda channel, request, timeout=5.0: order.append(request) or {'result': 'ok'}):
self.assertEqual(store.STORE_GAME, store.main([str(folder / store.LAUNCHER)]))
self.assertEqual([str(proton), 'run', 'cmd.exe', '/d', '/c', 'exit'], order[0])
self.assertEqual({'op': 'cloud-down', 'store': 'epic', 'id': 'fa4240e5'}, order[-1])
self.assertEqual('present', store.create_prefix(str(compat), str(proton)))
self.assertEqual(2, len(order))
def test_a_prefix_that_is_not_made_leaves_the_deferred_download(self):
with tempfile.TemporaryDirectory() as root:
proton = Path(root) / 'proton'
proton.write_text('selected Proton')
with patch.object(store.subprocess, 'run', return_value=__import__('subprocess').CompletedProcess([], 1)):
self.assertEqual('failed', store.create_prefix(root + '/compatdata/1', str(proton)))
with patch.object(store.subprocess, 'run', side_effect=__import__('subprocess').TimeoutExpired('proton', 1)):
self.assertEqual('timeout', store.create_prefix(root + '/compatdata/1', str(proton)))
self.assertEqual('no-proton', store.create_prefix(root + '/compatdata/1', root + '/missing'))
self.assertEqual('no-proton', store.create_prefix('', str(proton)))
def test_cloud_off_or_amazon_runs_proton_as_before(self):
with tempfile.TemporaryDirectory() as root:
folder = game(root, store_id='gog', ident='1')