mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-20 14:14:44 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55d5e2191c | ||
|
|
11892c7ad1 |
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", "")))
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user