Compare commits

...
6 changed files with 222 additions and 51 deletions

No files matched your search

+9
View File
@@ -151,6 +151,15 @@
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".ui.SecondScreenActivity"
android:exported="false"
android:excludeFromRecents="true"
android:theme="@style/SessionTheme"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".gpu.SteamVrRestartActivity"
android:exported="false"
@@ -107,7 +107,7 @@ import com.droiddeck.launcher.ui.HudText
import com.droiddeck.launcher.ui.LoadingOverlay
import com.droiddeck.launcher.ui.SessionDrawer
import com.droiddeck.launcher.ui.SessionPausedOverlay
import com.droiddeck.launcher.ui.SecondScreenPresentation
import com.droiddeck.launcher.ui.SecondScreenWindow
import com.droiddeck.launcher.ui.DroidDeckTheme
import com.droiddeck.launcher.wayland.CompositorHost
import com.droiddeck.launcher.wayland.WaylandCompositor
@@ -343,7 +343,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private var selectedSecondScreenDisplay by mutableStateOf(SessionState.secondScreenDisplay)
private var started = false
private lateinit var displayManager: DisplayManager
private var secondScreenPresentation: SecondScreenPresentation? = null
private var secondScreenPresentation: SecondScreenWindow? = null
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = refreshSecondScreenDisplays()
override fun onDisplayRemoved(displayId: Int) = refreshSecondScreenDisplays()
@@ -2118,7 +2118,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
var presentation = secondScreenPresentation
if (presentation?.display?.displayId != target.displayId) {
presentation?.closeControls()
presentation = SecondScreenPresentation(
presentation = SecondScreenWindow(
this, target,
sendPointer = ::moveSecondScreenPointer,
sendButton = { button, pressed ->
@@ -1425,6 +1425,7 @@ class SessionService : Service() {
}
override fun onTaskRemoved(rootIntent: Intent?) {
if (rootIntent?.component?.className == com.droiddeck.launcher.ui.SecondScreenActivity::class.java.name) return
if (pipTask || xrActivityVisible) {
Log.i(TAG, "panel task dismissed - retaining the session under its active surface policy")
activityVisible = false
@@ -1,21 +1,17 @@
package com.droiddeck.launcher.ui
import com.droiddeck.launcher.R
import android.app.Presentation
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.Display
import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewConfiguration
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.FrameLayout
@@ -37,9 +33,9 @@ import com.termux.terminal.TerminalSessionClient
import com.termux.view.TerminalView
import com.termux.view.TerminalViewClient
class SecondScreenPresentation(
internal class SecondScreenControls(
context: Context,
private val sessionActivity: SessionActivity,
display: Display,
private val sendPointer: (x: Float, y: Float, width: Float, height: Float) -> Unit,
private val sendButton: (button: Int, pressed: Boolean) -> Unit,
private val sendWheel: (steps: Int) -> Unit,
@@ -47,12 +43,8 @@ class SecondScreenPresentation(
private val onQam: () -> Unit,
private val onClose: () -> Unit,
private val pad: PadBridge? = null,
) : Presentation(sessionActivity, display) {
private val root = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(Color.rgb(15, 18, 22))
isFocusableInTouchMode = true
}
) : LinearLayout(context) {
private val root get() = this
private var currentMode = SecondScreenMode.NONE
private var keyboardHost: KeyboardHost? = null
private var terminalSession: TerminalSession? = null
@@ -60,35 +52,22 @@ class SecondScreenPresentation(
private var terminalPid = -1
private var ctrlActive = false
private var altActive = false
private var created = false
init {
setCancelable(false)
setCanceledOnTouchOutside(false)
setOnDismissListener { closeTerminal() }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
created = true
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContentView(root)
applyImmersiveMode()
renderMode()
orientation = VERTICAL
setBackgroundColor(Color.rgb(15, 18, 22))
isFocusableInTouchMode = true
}
fun showMode(mode: SecondScreenMode) {
if (currentMode == mode && isShowing) return
if (currentMode == mode) return
currentMode = mode
if (created) renderMode()
renderMode()
}
fun closeControls() {
closeTerminal()
keyboardHost?.hide()
keyboardHost = null
dismiss()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -98,14 +77,6 @@ class SecondScreenPresentation(
return super.dispatchKeyEvent(event)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
applyImmersiveMode()
sessionActivity.refreshClipboard()
}
}
private fun renderMode() {
keyboardHost?.hide()
keyboardHost = null
@@ -291,16 +262,6 @@ class SecondScreenPresentation(
private fun weightParams() = LinearLayout.LayoutParams(-1, 0, 1f)
private fun applyImmersiveMode() {
@Suppress("DEPRECATION")
window?.decorView?.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
private inner class TrackpadView(context: Context) : View(context) {
private val gestures = TouchpadGestures(ViewConfiguration.get(context).scaledTouchSlop.toFloat(),
object : PointerGestures.Listener {
@@ -0,0 +1,139 @@
package com.droiddeck.launcher.ui
import android.app.Activity
import android.app.ActivityOptions
import android.content.Intent
import android.os.Bundle
import android.view.Display
import android.view.View
import android.view.WindowManager
import com.droiddeck.launcher.SessionActivity
import com.droiddeck.launcher.input.PadBridge
import com.droiddeck.launcher.input.SecondScreenMode
import com.droiddeck.launcher.session.SessionState
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicLong
class SecondScreenWindow(
private val sessionActivity: SessionActivity,
val display: Display,
private val sendPointer: (Float, Float, Float, Float) -> Unit,
private val sendButton: (Int, Boolean) -> Unit,
private val sendWheel: (Int) -> Unit,
private val onSteamMenu: () -> Unit,
private val onQam: () -> Unit,
private val onClose: () -> Unit,
private val pad: PadBridge? = null,
) {
private val token = tokens.incrementAndGet()
private var activity = WeakReference<SecondScreenActivity>(null)
private var controls: SecondScreenControls? = null
private var mode = SecondScreenMode.NONE
private var launching = false
private var closed = false
val window get() = activity.get()?.window
val isShowing get() = !closed && (launching || activity.get()?.isFinishing == false)
fun show() {
if (isShowing || closed) return
owners[token] = WeakReference(this)
launching = true
try {
val intent = Intent(sessionActivity, SecondScreenActivity::class.java)
.putExtra(OWNER, token)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
sessionActivity.startActivity(intent, ActivityOptions.makeBasic().setLaunchDisplayId(display.displayId).toBundle())
} catch (error: Exception) {
launching = false
owners.remove(token)
throw error
}
}
fun showMode(mode: SecondScreenMode) {
this.mode = mode
controls?.showMode(mode)
}
fun closeControls() {
closed = true
launching = false
owners.remove(token)
controls?.closeControls()
controls = null
activity.get()?.finishAndRemoveTask()
activity.clear()
}
internal fun attach(host: SecondScreenActivity): View {
controls?.closeControls()
activity = WeakReference(host)
launching = false
return SecondScreenControls(host, sessionActivity, sendPointer, sendButton, sendWheel,
onSteamMenu, onQam, onClose, pad).also {
controls = it
it.showMode(mode)
}
}
internal fun detach(host: SecondScreenActivity) {
if (activity.get() !== host) return
controls?.closeControls()
controls = null
activity.clear()
if (host.isFinishing && !closed) onClose()
}
internal fun refreshClipboard() = sessionActivity.refreshClipboard()
companion object {
private const val OWNER = "secondScreenOwner"
private val tokens = AtomicLong()
private val owners = mutableMapOf<Long, WeakReference<SecondScreenWindow>>()
internal fun find(intent: Intent): SecondScreenWindow? =
owners[intent.getLongExtra(OWNER, 0)]?.get()?.takeUnless { it.closed }
}
}
class SecondScreenActivity : Activity() {
private var owner: SecondScreenWindow? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val host = SecondScreenWindow.find(intent)
if (host == null || !SessionState.running) {
finishAndRemoveTask()
return
}
owner = host
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContentView(host.attach(this))
applyImmersiveMode()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
applyImmersiveMode()
owner?.refreshClipboard()
}
}
@Deprecated("Back is handled by the session controls")
override fun onBackPressed() = Unit
override fun onDestroy() {
owner?.detach(this)
owner = null
super.onDestroy()
}
private fun applyImmersiveMode() {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
}
@@ -0,0 +1,61 @@
package com.droiddeck.launcher.ui
import android.app.Application
import android.content.Intent
import android.hardware.display.DisplayManager
import android.view.Display
import com.droiddeck.launcher.SessionActivity
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class, manifest = Config.NONE, sdk = [28, 33])
@LooperMode(LooperMode.Mode.PAUSED)
class SecondScreenActivityTest {
@Before fun setUp() {
SessionState.running = true
SessionState.stopRequested = false
}
@After fun tearDown() {
SessionState.running = false
SessionState.stopRequested = false
}
@Test fun removingControlsTaskDoesNotStopTheRunningSession() {
val service = Robolectric.buildService(SessionService::class.java).get()
service.onTaskRemoved(Intent(service, SecondScreenActivity::class.java))
assertTrue(SessionState.running)
assertFalse(SessionState.stopRequested)
}
@Test fun activityRestoredWithoutItsSessionOwnerFinishes() {
val activity = Robolectric.buildActivity(SecondScreenActivity::class.java).create().get()
assertTrue(activity.isFinishing)
assertTrue(SessionState.running)
}
@Test fun closingBeforeTheSecondaryActivityStartsRejectsTheLateLaunch() {
val session = Robolectric.buildActivity(SessionActivity::class.java).get()
val display = session.getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY)
val controls = SecondScreenWindow(session, display, { _, _, _, _ -> }, { _, _ -> }, {}, {}, {}, {})
controls.show()
val intent = shadowOf(session).nextStartedActivity
controls.closeControls()
val activity = Robolectric.buildActivity(SecondScreenActivity::class.java, intent).create().get()
assertTrue(activity.isFinishing)
assertFalse(controls.isShowing)
assertTrue(SessionState.running)
}
}