Refactor Android Input and add Face Button Swap

This commit is contained in:
Florian Märkl 2019-10-30 10:59:22 +01:00
commit bd02acb5a0
9 changed files with 311 additions and 268 deletions

View file

@ -1,228 +0,0 @@
/*
* This file is part of Chiaki.
*
* Chiaki is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chiaki is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Chiaki. If not, see <https://www.gnu.org/licenses/>.
*/
package com.metallic.chiaki
import android.graphics.SurfaceTexture
import android.util.Log
import android.view.*
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.metallic.chiaki.lib.*
sealed class StreamState
object StreamStateIdle: StreamState()
object StreamStateConnecting: StreamState()
object StreamStateConnected: StreamState()
data class StreamStateCreateError(val error: CreateError): StreamState()
data class StreamStateQuit(val reason: QuitReason, val reasonString: String?): StreamState()
data class StreamStateLoginPinRequest(val pinIncorrect: Boolean): StreamState()
class StreamSession(val connectInfo: ConnectInfo)
{
var session: Session? = null
private set
private val _state = MutableLiveData<StreamState>(StreamStateIdle)
val state: LiveData<StreamState> get() = _state
private val keyControllerState = ControllerState() // from KeyEvents
private val motionControllerState = ControllerState() // from MotionEvents
private var touchControllerState = ControllerState()
var surfaceTexture: SurfaceTexture? = null
fun shutdown()
{
session?.stop()
session?.dispose()
session = null
_state.value = StreamStateIdle
//surfaceTexture?.release()
}
fun pause()
{
shutdown()
}
fun resume()
{
if(session != null)
return
try
{
val session = Session(connectInfo)
_state.value = StreamStateConnecting
session.eventCallback = this::eventCallback
session.start()
val surfaceTexture = surfaceTexture
if(surfaceTexture != null)
session.setSurface(Surface(surfaceTexture))
this.session = session
}
catch(e: CreateError)
{
_state.value = StreamStateCreateError(e)
}
}
private fun eventCallback(event: Event)
{
when(event)
{
is ConnectedEvent -> _state.postValue(StreamStateConnected)
is QuitEvent -> _state.postValue(StreamStateQuit(event.reason, event.reasonString))
is LoginPinRequestEvent -> _state.postValue(StreamStateLoginPinRequest(event.pinIncorrect))
}
}
fun attachToTextureView(textureView: TextureView)
{
textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int)
{
if(surfaceTexture != null)
return
surfaceTexture = surface
session?.setSurface(Surface(surface))
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean
{
// return false if we want to keep the surface texture
return surfaceTexture == null
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { }
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}
val surfaceTexture = surfaceTexture
if(surfaceTexture != null)
textureView.surfaceTexture = surfaceTexture
}
fun setLoginPin(pin: String)
{
session?.setLoginPin(pin)
}
fun dispatchKeyEvent(event: KeyEvent): Boolean
{
Log.i("StreamSession", "key event $event")
if(event.action != KeyEvent.ACTION_DOWN && event.action != KeyEvent.ACTION_UP)
return false
when(event.keyCode)
{
KeyEvent.KEYCODE_BUTTON_L2 -> {
keyControllerState.l2State = if(event.action == KeyEvent.ACTION_DOWN) UByte.MAX_VALUE else 0U
return true
}
KeyEvent.KEYCODE_BUTTON_R2 -> {
keyControllerState.r2State = if(event.action == KeyEvent.ACTION_DOWN) UByte.MAX_VALUE else 0U
return true
}
}
val buttonMask: UInt = when(event.keyCode)
{
// dpad handled by MotionEvents
//KeyEvent.KEYCODE_DPAD_LEFT -> ControllerState.BUTTON_DPAD_LEFT
//KeyEvent.KEYCODE_DPAD_RIGHT -> ControllerState.BUTTON_DPAD_RIGHT
//KeyEvent.KEYCODE_DPAD_UP -> ControllerState.BUTTON_DPAD_UP
//KeyEvent.KEYCODE_DPAD_DOWN -> ControllerState.BUTTON_DPAD_DOWN
KeyEvent.KEYCODE_BUTTON_A -> ControllerState.BUTTON_CROSS
KeyEvent.KEYCODE_BUTTON_B -> ControllerState.BUTTON_MOON
KeyEvent.KEYCODE_BUTTON_X -> ControllerState.BUTTON_BOX
KeyEvent.KEYCODE_BUTTON_Y -> ControllerState.BUTTON_PYRAMID
KeyEvent.KEYCODE_BUTTON_L1 -> ControllerState.BUTTON_L1
KeyEvent.KEYCODE_BUTTON_R1 -> ControllerState.BUTTON_R1
KeyEvent.KEYCODE_BUTTON_THUMBL -> ControllerState.BUTTON_L3
KeyEvent.KEYCODE_BUTTON_THUMBR -> ControllerState.BUTTON_R3
KeyEvent.KEYCODE_BUTTON_SELECT -> ControllerState.BUTTON_SHARE
KeyEvent.KEYCODE_BUTTON_START -> ControllerState.BUTTON_OPTIONS
KeyEvent.KEYCODE_BUTTON_C -> ControllerState.BUTTON_PS
KeyEvent.KEYCODE_BUTTON_MODE -> ControllerState.BUTTON_PS
else -> return false
}
keyControllerState.buttons = keyControllerState.buttons.run {
when(event.action)
{
KeyEvent.ACTION_DOWN -> this or buttonMask
KeyEvent.ACTION_UP -> this and buttonMask.inv()
else -> this
}
}
sendControllerState()
return true
}
fun onGenericMotionEvent(event: MotionEvent): Boolean
{
if(event.source and InputDevice.SOURCE_CLASS_JOYSTICK != InputDevice.SOURCE_CLASS_JOYSTICK)
return false
fun Float.signedAxis() = (this * Short.MAX_VALUE).toShort()
fun Float.unsignedAxis() = (this * UByte.MAX_VALUE.toFloat()).toUInt().toUByte()
motionControllerState.leftX = event.getAxisValue(MotionEvent.AXIS_X).signedAxis()
motionControllerState.leftY = event.getAxisValue(MotionEvent.AXIS_Y).signedAxis()
motionControllerState.rightX = event.getAxisValue(MotionEvent.AXIS_Z).signedAxis()
motionControllerState.rightY = event.getAxisValue(MotionEvent.AXIS_RZ).signedAxis()
motionControllerState.l2State = event.getAxisValue(MotionEvent.AXIS_LTRIGGER).unsignedAxis()
motionControllerState.r2State = event.getAxisValue(MotionEvent.AXIS_RTRIGGER).unsignedAxis()
motionControllerState.buttons = motionControllerState.buttons.let {
val dpadX = event.getAxisValue(MotionEvent.AXIS_HAT_X)
val dpadY = event.getAxisValue(MotionEvent.AXIS_HAT_Y)
val dpadButtons =
(if(dpadX > 0.5f) ControllerState.BUTTON_DPAD_RIGHT else 0U) or
(if(dpadX < -0.5f) ControllerState.BUTTON_DPAD_LEFT else 0U) or
(if(dpadY > 0.5f) ControllerState.BUTTON_DPAD_DOWN else 0U) or
(if(dpadY < -0.5f) ControllerState.BUTTON_DPAD_UP else 0U)
it and (ControllerState.BUTTON_DPAD_RIGHT or
ControllerState.BUTTON_DPAD_LEFT or
ControllerState.BUTTON_DPAD_DOWN or
ControllerState.BUTTON_DPAD_UP).inv() or
dpadButtons
}
Log.i("StreamSession", "motionEvent => $motionControllerState")
sendControllerState()
return true
}
fun updateTouchControllerState(controllerState: ControllerState)
{
touchControllerState = controllerState
sendControllerState()
}
private fun sendControllerState()
{
val controllerState = keyControllerState or motionControllerState
// prioritize motion controller's l2 and r2 over key
// (some controllers send only key, others both but key earlier than full press)
if(motionControllerState.l2State > 0U)
controllerState.l2State = motionControllerState.l2State
if(motionControllerState.r2State > 0U)
controllerState.r2State = motionControllerState.r2State
session?.setControllerState(controllerState or touchControllerState)
}
}

View file

@ -79,6 +79,11 @@ class Preferences(context: Context)
get() = sharedPreferences.getBoolean(logVerboseKey, false) get() = sharedPreferences.getBoolean(logVerboseKey, false)
set(value) { sharedPreferences.edit().putBoolean(logVerboseKey, value).apply() } set(value) { sharedPreferences.edit().putBoolean(logVerboseKey, value).apply() }
val swapCrossMoonKey get() = resources.getString(R.string.preferences_swap_cross_moon_key)
var swapCrossMoon
get() = sharedPreferences.getBoolean(swapCrossMoonKey, false)
set(value) { sharedPreferences.edit().putBoolean(swapCrossMoonKey, value).apply() }
val resolutionKey get() = resources.getString(R.string.preferences_resolution_key) val resolutionKey get() = resources.getString(R.string.preferences_resolution_key)
var resolution var resolution
get() = sharedPreferences.getString(resolutionKey, resolutionDefault.value)?.let { value -> get() = sharedPreferences.getString(resolutionKey, resolutionDefault.value)?.let { value ->

View file

@ -0,0 +1,127 @@
package com.metallic.chiaki.session
import android.util.Log
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import com.metallic.chiaki.common.Preferences
import com.metallic.chiaki.lib.ControllerState
class StreamInput(val preferences: Preferences)
{
var controllerStateChangedCallback: ((ControllerState) -> Unit)? = null
val controllerState: ControllerState get()
{
val controllerState = keyControllerState or motionControllerState
// prioritize motion controller's l2 and r2 over key
// (some controllers send only key, others both but key earlier than full press)
if(motionControllerState.l2State > 0U)
controllerState.l2State = motionControllerState.l2State
if(motionControllerState.r2State > 0U)
controllerState.r2State = motionControllerState.r2State
return controllerState or touchControllerState
}
private val keyControllerState = ControllerState() // from KeyEvents
private val motionControllerState = ControllerState() // from MotionEvents
var touchControllerState = ControllerState()
set(value)
{
field = value
controllerStateUpdated()
}
private val swapCrossMoon = preferences.swapCrossMoon
private fun controllerStateUpdated()
{
controllerStateChangedCallback?.let { it(controllerState) }
}
fun dispatchKeyEvent(event: KeyEvent): Boolean
{
Log.i("StreamSession", "key event $event")
if(event.action != KeyEvent.ACTION_DOWN && event.action != KeyEvent.ACTION_UP)
return false
when(event.keyCode)
{
KeyEvent.KEYCODE_BUTTON_L2 -> {
keyControllerState.l2State = if(event.action == KeyEvent.ACTION_DOWN) UByte.MAX_VALUE else 0U
return true
}
KeyEvent.KEYCODE_BUTTON_R2 -> {
keyControllerState.r2State = if(event.action == KeyEvent.ACTION_DOWN) UByte.MAX_VALUE else 0U
return true
}
}
val buttonMask: UInt = when(event.keyCode)
{
// dpad handled by MotionEvents
//KeyEvent.KEYCODE_DPAD_LEFT -> ControllerState.BUTTON_DPAD_LEFT
//KeyEvent.KEYCODE_DPAD_RIGHT -> ControllerState.BUTTON_DPAD_RIGHT
//KeyEvent.KEYCODE_DPAD_UP -> ControllerState.BUTTON_DPAD_UP
//KeyEvent.KEYCODE_DPAD_DOWN -> ControllerState.BUTTON_DPAD_DOWN
KeyEvent.KEYCODE_BUTTON_A -> if(swapCrossMoon) ControllerState.BUTTON_MOON else ControllerState.BUTTON_CROSS
KeyEvent.KEYCODE_BUTTON_B -> if(swapCrossMoon) ControllerState.BUTTON_CROSS else ControllerState.BUTTON_MOON
KeyEvent.KEYCODE_BUTTON_X -> if(swapCrossMoon) ControllerState.BUTTON_PYRAMID else ControllerState.BUTTON_BOX
KeyEvent.KEYCODE_BUTTON_Y -> if(swapCrossMoon) ControllerState.BUTTON_BOX else ControllerState.BUTTON_PYRAMID
KeyEvent.KEYCODE_BUTTON_L1 -> ControllerState.BUTTON_L1
KeyEvent.KEYCODE_BUTTON_R1 -> ControllerState.BUTTON_R1
KeyEvent.KEYCODE_BUTTON_THUMBL -> ControllerState.BUTTON_L3
KeyEvent.KEYCODE_BUTTON_THUMBR -> ControllerState.BUTTON_R3
KeyEvent.KEYCODE_BUTTON_SELECT -> ControllerState.BUTTON_SHARE
KeyEvent.KEYCODE_BUTTON_START -> ControllerState.BUTTON_OPTIONS
KeyEvent.KEYCODE_BUTTON_C -> ControllerState.BUTTON_PS
KeyEvent.KEYCODE_BUTTON_MODE -> ControllerState.BUTTON_PS
else -> return false
}
keyControllerState.buttons = keyControllerState.buttons.run {
when(event.action)
{
KeyEvent.ACTION_DOWN -> this or buttonMask
KeyEvent.ACTION_UP -> this and buttonMask.inv()
else -> this
}
}
controllerStateUpdated()
return true
}
fun onGenericMotionEvent(event: MotionEvent): Boolean
{
if(event.source and InputDevice.SOURCE_CLASS_JOYSTICK != InputDevice.SOURCE_CLASS_JOYSTICK)
return false
fun Float.signedAxis() = (this * Short.MAX_VALUE).toShort()
fun Float.unsignedAxis() = (this * UByte.MAX_VALUE.toFloat()).toUInt().toUByte()
motionControllerState.leftX = event.getAxisValue(MotionEvent.AXIS_X).signedAxis()
motionControllerState.leftY = event.getAxisValue(MotionEvent.AXIS_Y).signedAxis()
motionControllerState.rightX = event.getAxisValue(MotionEvent.AXIS_Z).signedAxis()
motionControllerState.rightY = event.getAxisValue(MotionEvent.AXIS_RZ).signedAxis()
motionControllerState.l2State = event.getAxisValue(MotionEvent.AXIS_LTRIGGER).unsignedAxis()
motionControllerState.r2State = event.getAxisValue(MotionEvent.AXIS_RTRIGGER).unsignedAxis()
motionControllerState.buttons = motionControllerState.buttons.let {
val dpadX = event.getAxisValue(MotionEvent.AXIS_HAT_X)
val dpadY = event.getAxisValue(MotionEvent.AXIS_HAT_Y)
val dpadButtons =
(if(dpadX > 0.5f) ControllerState.BUTTON_DPAD_RIGHT else 0U) or
(if(dpadX < -0.5f) ControllerState.BUTTON_DPAD_LEFT else 0U) or
(if(dpadY > 0.5f) ControllerState.BUTTON_DPAD_DOWN else 0U) or
(if(dpadY < -0.5f) ControllerState.BUTTON_DPAD_UP else 0U)
it and (ControllerState.BUTTON_DPAD_RIGHT or
ControllerState.BUTTON_DPAD_LEFT or
ControllerState.BUTTON_DPAD_DOWN or
ControllerState.BUTTON_DPAD_UP).inv() or
dpadButtons
}
//Log.i("StreamSession", "motionEvent => $motionControllerState")
controllerStateUpdated()
return true
}
}

View file

@ -0,0 +1,136 @@
/*
* This file is part of Chiaki.
*
* Chiaki is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chiaki is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Chiaki. If not, see <https://www.gnu.org/licenses/>.
*/
package com.metallic.chiaki.session
import android.graphics.SurfaceTexture
import android.util.Log
import android.view.*
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.metallic.chiaki.lib.*
sealed class StreamState
object StreamStateIdle: StreamState()
object StreamStateConnecting: StreamState()
object StreamStateConnected: StreamState()
data class StreamStateCreateError(val error: CreateError): StreamState()
data class StreamStateQuit(val reason: QuitReason, val reasonString: String?): StreamState()
data class StreamStateLoginPinRequest(val pinIncorrect: Boolean): StreamState()
class StreamSession(val connectInfo: ConnectInfo, val input: StreamInput)
{
var session: Session? = null
private set
private val _state = MutableLiveData<StreamState>(StreamStateIdle)
val state: LiveData<StreamState> get() = _state
var surfaceTexture: SurfaceTexture? = null
init
{
input.controllerStateChangedCallback = {
session?.setControllerState(it)
}
}
fun shutdown()
{
session?.stop()
session?.dispose()
session = null
_state.value = StreamStateIdle
//surfaceTexture?.release()
}
fun pause()
{
shutdown()
}
fun resume()
{
if(session != null)
return
try
{
val session = Session(connectInfo)
_state.value = StreamStateConnecting
session.eventCallback = this::eventCallback
session.start()
val surfaceTexture = surfaceTexture
if(surfaceTexture != null)
session.setSurface(Surface(surfaceTexture))
this.session = session
}
catch(e: CreateError)
{
_state.value = StreamStateCreateError(e)
}
}
private fun eventCallback(event: Event)
{
when(event)
{
is ConnectedEvent -> _state.postValue(StreamStateConnected)
is QuitEvent -> _state.postValue(
StreamStateQuit(
event.reason,
event.reasonString
)
)
is LoginPinRequestEvent -> _state.postValue(
StreamStateLoginPinRequest(
event.pinIncorrect
)
)
}
}
fun attachToTextureView(textureView: TextureView)
{
textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int)
{
if(surfaceTexture != null)
return
surfaceTexture = surface
session?.setSurface(Surface(surface))
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean
{
// return false if we want to keep the surface texture
return surfaceTexture == null
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { }
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}
val surfaceTexture = surfaceTexture
if(surfaceTexture != null)
textureView.surfaceTexture = surfaceTexture
}
fun setLoginPin(pin: String)
{
session?.setLoginPin(pin)
}
}

View file

@ -20,14 +20,9 @@ package com.metallic.chiaki.stream
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.app.AlertDialog import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Matrix import android.graphics.Matrix
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.speech.tts.TextToSpeech
import android.text.method.Touch
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@ -38,13 +33,11 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.* import androidx.lifecycle.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.metallic.chiaki.*
import com.metallic.chiaki.R import com.metallic.chiaki.R
import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.common.Preferences
import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.ext.viewModelFactory
import com.metallic.chiaki.lib.ConnectInfo import com.metallic.chiaki.lib.ConnectInfo
import com.metallic.chiaki.lib.LoginPinRequestEvent import com.metallic.chiaki.session.*
import com.metallic.chiaki.lib.QuitReason
import com.metallic.chiaki.touchcontrols.TouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchControlsFragment
import kotlinx.android.synthetic.main.activity_stream.* import kotlinx.android.synthetic.main.activity_stream.*
@ -68,18 +61,17 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
{ {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory { StreamViewModel(Preferences(this)) })[StreamViewModel::class.java] val connectInfo = intent.getParcelableExtra<ConnectInfo>(EXTRA_CONNECT_INFO)
if(!viewModel.isInitialized) if(connectInfo == null)
{ {
val connectInfo = intent.getParcelableExtra<ConnectInfo>(EXTRA_CONNECT_INFO) finish()
if(connectInfo == null) return
{
finish()
return
}
viewModel.init(connectInfo)
} }
viewModel = ViewModelProviders.of(this, viewModelFactory {
StreamViewModel(Preferences(this), connectInfo)
})[StreamViewModel::class.java]
setContentView(R.layout.activity_stream) setContentView(R.layout.activity_stream)
window.decorView.setOnSystemUiVisibilityChangeListener(this) window.decorView.setOnSystemUiVisibilityChangeListener(this)
@ -104,7 +96,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
super.onAttachFragment(fragment) super.onAttachFragment(fragment)
if(fragment is TouchControlsFragment) if(fragment is TouchControlsFragment)
{ {
fragment.controllerStateCallback = viewModel.session::updateTouchControllerState fragment.controllerStateCallback = { viewModel.input.touchControllerState = it }
fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled
} }
} }
@ -309,6 +301,6 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
} }
} }
override fun dispatchKeyEvent(event: KeyEvent) = viewModel.session.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
override fun onGenericMotionEvent(event: MotionEvent) = viewModel.session.onGenericMotionEvent(event) || super.onGenericMotionEvent(event) override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event)
} }

View file

@ -20,27 +20,20 @@ package com.metallic.chiaki.stream
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.metallic.chiaki.StreamSession import com.metallic.chiaki.session.StreamSession
import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.common.Preferences
import com.metallic.chiaki.lib.* import com.metallic.chiaki.lib.*
import com.metallic.chiaki.session.StreamInput
class StreamViewModel(val preferences: Preferences): ViewModel() class StreamViewModel(val preferences: Preferences, val connectInfo: ConnectInfo): ViewModel()
{ {
private var connectInfo: ConnectInfo? = null
private var _session: StreamSession? = null private var _session: StreamSession? = null
val session: StreamSession get() = _session ?: throw UninitializedPropertyAccessException("StreamViewModel not initialized") val input = StreamInput(preferences)
val isInitialized get() = connectInfo != null val session = StreamSession(connectInfo, input)
private var _onScreenControlsEnabled = MutableLiveData<Boolean>(preferences.onScreenControlsEnabled) private var _onScreenControlsEnabled = MutableLiveData<Boolean>(preferences.onScreenControlsEnabled)
val onScreenControlsEnabled: LiveData<Boolean> get() = _onScreenControlsEnabled val onScreenControlsEnabled: LiveData<Boolean> get() = _onScreenControlsEnabled
fun init(connectInfo: ConnectInfo)
{
if(isInitialized)
return
_session = StreamSession(connectInfo)
}
override fun onCleared() override fun onCleared()
{ {
super.onCleared() super.onCleared()

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?android:attr/textColorPrimary"
android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

View file

@ -58,6 +58,15 @@
<string name="preferences_bitrate_title">Bitrate</string> <string name="preferences_bitrate_title">Bitrate</string>
<string name="preferences_log_verbose_title">Verbose Logging</string> <string name="preferences_log_verbose_title">Verbose Logging</string>
<string name="preferences_log_verbose_summary">Warning: This logs a LOT! Don\'t enable for regular use.</string> <string name="preferences_log_verbose_summary">Warning: This logs a LOT! Don\'t enable for regular use.</string>
<string name="preferences_resolution_title_360p">360p</string>
<string name="preferences_resolution_title_540p">540p</string>
<string name="preferences_resolution_title_720p">720p</string>
<string name="preferences_resolution_title_1080p">1080p</string>
<string name="preferences_fps_title_30">30</string>
<string name="preferences_fps_title_60">60</string>
<string name="preferences_bitrate_auto">Auto (%d)</string>
<string name="preferences_swap_cross_moon_title">Swap Cross/Moon and Box/Pyramid Buttons</string>
<string name="preferences_swap_cross_moon_summary">Swap face buttons if default mapping is incorrect (e.g. for 8BitDo controllers)</string>
<string name="alert_message_delete_registered_host">Are you sure you want to delete the registered console %s with ID %s?</string> <string name="alert_message_delete_registered_host">Are you sure you want to delete the registered console %s with ID %s?</string>
<string name="alert_message_delete_manual_host">Are you sure you want to delete the console entry for %s?</string> <string name="alert_message_delete_manual_host">Are you sure you want to delete the console entry for %s?</string>
<string name="action_keep">Keep</string> <string name="action_keep">Keep</string>
@ -68,14 +77,8 @@
<string name="preferences_discovery_enabled_key">discovery_enabled</string> <string name="preferences_discovery_enabled_key">discovery_enabled</string>
<string name="preferences_on_screen_controls_enabled_key">on_screen_controls_enabled</string> <string name="preferences_on_screen_controls_enabled_key">on_screen_controls_enabled</string>
<string name="preferences_log_verbose_key">log_verbose</string> <string name="preferences_log_verbose_key">log_verbose</string>
<string name="preferences_swap_cross_moon_key">swap_cross_moon</string>
<string name="preferences_resolution_key">stream_resolution</string> <string name="preferences_resolution_key">stream_resolution</string>
<string name="preferences_resolution_title_360p">360p</string>
<string name="preferences_resolution_title_540p">540p</string>
<string name="preferences_resolution_title_720p">720p</string>
<string name="preferences_resolution_title_1080p">1080p</string>
<string name="preferences_fps_key">stream_fps</string> <string name="preferences_fps_key">stream_fps</string>
<string name="preferences_fps_title_30">30</string>
<string name="preferences_fps_title_60">60</string>
<string name="preferences_bitrate_key">stream_bitrate</string> <string name="preferences_bitrate_key">stream_bitrate</string>
<string name="preferences_bitrate_auto">Auto (%d)</string>
</resources> </resources>

View file

@ -14,10 +14,16 @@
app:icon="@drawable/ic_console_simple"/> app:icon="@drawable/ic_console_simple"/>
<SwitchPreference <SwitchPreference
app:key="log_verbose" app:key="@string/preferences_log_verbose_key"
app:title="@string/preferences_log_verbose_title" app:title="@string/preferences_log_verbose_title"
app:summary="@string/preferences_log_verbose_summary" app:summary="@string/preferences_log_verbose_summary"
app:icon="@drawable/ic_log" /> app:icon="@drawable/ic_log" />
<SwitchPreference
app:key="@string/preferences_swap_cross_moon_key"
app:title="@string/preferences_swap_cross_moon_title"
app:summary="@string/preferences_swap_cross_moon_summary"
app:icon="@drawable/ic_gamepad" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory