Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video output controller profile #41

Merged
merged 19 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* text eol=lf
* text=auto
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ jobs:
with:
add-job-summary-as-pr-comment: always
build-scan-publish: true
build-scan-terms-of-service-url: "https://gradle.com/terms-of-service"
build-scan-terms-of-service-agree: "yes"
build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use"
build-scan-terms-of-use-agree: "yes"
- name: Build app
run: ./gradlew assemble
- name: Save debug apk
Expand Down
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,8 @@ dependencies {
ksp(libs.bundles.appKsp)
testImplementation(libs.bundles.appUnitTest)
androidTestImplementation(libs.bundles.appAndroidTest)

// Hilt dependencies
implementation(libs.com.google.dagger.hilt.android)
ksp(libs.com.google.dagger.hilt.android.compiler)
langerhans marked this conversation as resolved.
Show resolved Hide resolved
}
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="com.odin2.permission.RECEIVE_VIDEO_OUTPUT_PORT_CHANGE"/>
<uses-permission android:name="com.retrostation.permission.RECEIVE_VIDEO_OUTPUT_PORT_CHANGE"/>

<application
android:name=".OdinToolsApplication"
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/java/de/langerhans/odintools/data/SharedPrefsRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package de.langerhans.odintools.data
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import dagger.hilt.android.qualifiers.ApplicationContext
import de.langerhans.odintools.models.ControllerStyle
import de.langerhans.odintools.models.L2R2Style
import javax.inject.Inject

class SharedPrefsRepo @Inject constructor(
Expand Down Expand Up @@ -84,6 +86,34 @@ class SharedPrefsRepo @Inject constructor(
appOverrideEnabledListener = null
}

var videoOutputOverrideEnabled
get() = prefs.getBoolean(KEY_VIDEO_OUTPUT_OVERRIDE_ENABLED, false)
set(value) = prefs.edit().putBoolean(KEY_VIDEO_OUTPUT_OVERRIDE_ENABLED, value).apply()

var videoOutputControllerStyle
get() = prefs.getString(KEY_VIDEO_OUTPUT_CONTROLLER_STYLE, ControllerStyle.Unknown.id)
set(value) = prefs.edit().putString(KEY_VIDEO_OUTPUT_CONTROLLER_STYLE, value).apply()

var videoOutputL2R2Style
get() = prefs.getString(KEY_VIDEO_OUTPUT_L2R2_STYLE, L2R2Style.Unknown.id)
set(value) = prefs.edit().putString(KEY_VIDEO_OUTPUT_L2R2_STYLE, value).apply()

private var videoOutputOverrideEnabledListener: OnSharedPreferenceChangeListener? = null

fun observeVideoOutputOverrideEnabledState(onVideoOutputOverrideEnabled: (newState: Boolean) -> Unit) {
videoOutputOverrideEnabledListener = OnSharedPreferenceChangeListener { _, key ->
if (key == KEY_VIDEO_OUTPUT_OVERRIDE_ENABLED) {
onVideoOutputOverrideEnabled(videoOutputOverrideEnabled)
}
}
prefs.registerOnSharedPreferenceChangeListener(videoOutputOverrideEnabledListener)
}

fun removeVideoOutputOverrideEnabledObserver() {
prefs.unregisterOnSharedPreferenceChangeListener(videoOutputOverrideEnabledListener)
videoOutputOverrideEnabledListener = null
}

companion object {
private const val PREFS_NAME = "odintools"

Expand All @@ -96,5 +126,8 @@ class SharedPrefsRepo @Inject constructor(
private const val KEY_CHARGE_LIMIT_ENABLED = "charge_limit_enabled"
private const val KEY_MIN_BATTERY_LEVEL = "min_battery_level"
private const val KEY_MAX_BATTERY_LEVEL = "max_battery_level"
private const val KEY_VIDEO_OUTPUT_OVERRIDE_ENABLED = "video_output_override_enabled"
private const val KEY_VIDEO_OUTPUT_CONTROLLER_STYLE = "video_output_override_controller_style"
private const val KEY_VIDEO_OUTPUT_L2R2_STYLE = "video_output_override_l2r2_style"
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/de/langerhans/odintools/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import de.langerhans.odintools.ui.composables.SwitchPreference
import de.langerhans.odintools.ui.composables.SwitchableTriggerPreference
import de.langerhans.odintools.ui.composables.TriggerPreference
import de.langerhans.odintools.ui.composables.VibrationPreferenceDialog
import de.langerhans.odintools.ui.composables.VideoOutputOverridePreferenceDialog
import de.langerhans.odintools.ui.theme.OdinToolsTheme

@AndroidEntryPoint
Expand Down Expand Up @@ -117,6 +118,17 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid
)
}

if (uiState.showVideoOutputOverrideDialog) {
VideoOutputOverridePreferenceDialog(
initialControllerStyle = uiState.videoOutputControllerStyle,
initialL2R2Style = uiState.videoOutputL2R2Style,
onCancel = { viewModel.videoOutputOverrideDialogDismissed() },
onSave = { newControllerStyle, newL2R2Style ->
viewModel.saveVideoOutputOverride(newControllerStyle, newL2R2Style)
},
)
}

if (uiState.showVibrationDialog) {
VibrationPreferenceDialog(
initialValue = uiState.currentVibration,
Expand Down Expand Up @@ -159,6 +171,15 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid
) { newValue ->
viewModel.appOverridesEnabled(newValue)
}
SwitchableTriggerPreference(
icon = R.drawable.ic_gamepad_docked,
title = R.string.videoOutputOverride,
description = R.string.videoOutputOverrideDescription,
state = uiState.videoOutputOverrideEnabled,
onClick = { viewModel.videoOutputOverrideClicked() },
) {
viewModel.updateVideoOutputOverridePreference(it)
}
SwitchPreference(
icon = R.drawable.ic_more_time,
title = R.string.overrideDelay,
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/de/langerhans/odintools/main/MainUiModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import de.langerhans.odintools.models.ControllerStyle
import de.langerhans.odintools.models.L2R2Style
import de.langerhans.odintools.tools.DeviceType
import de.langerhans.odintools.tools.DeviceType.ODIN2

Expand All @@ -23,6 +25,11 @@ data class MainUiModel(
val showSaturationDialog: Boolean = false,
val currentSaturation: Float = 1.0f,

val showVideoOutputOverrideDialog: Boolean = false,
val videoOutputOverrideEnabled: Boolean = false,
val videoOutputControllerStyle: ControllerStyle = ControllerStyle.Unknown,
val videoOutputL2R2Style: L2R2Style = L2R2Style.Unknown,

val showVibrationDialog: Boolean = false,
val vibrationEnabled: Boolean = false,
val currentVibration: Int = 0,
Expand Down
39 changes: 38 additions & 1 deletion app/src/main/java/de/langerhans/odintools/main/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import de.langerhans.odintools.R
import de.langerhans.odintools.data.SharedPrefsRepo
import de.langerhans.odintools.models.ControllerStyle
import de.langerhans.odintools.models.ControllerStyle.Disconnect
import de.langerhans.odintools.models.ControllerStyle.Odin
import de.langerhans.odintools.models.ControllerStyle.Xbox
import de.langerhans.odintools.models.L2R2Style
import de.langerhans.odintools.models.L2R2Style.Analog
import de.langerhans.odintools.models.L2R2Style.Both
import de.langerhans.odintools.models.L2R2Style.Digital
Expand Down Expand Up @@ -58,8 +60,8 @@ class MainViewModel @Inject constructor(
showPServerNotAvailableDialog = !executor.pServerAvailable,
overrideDelayEnabled = prefs.overrideDelay,
vibrationEnabled = settings.vibrationEnabled,
currentVibration = settings.vibrationStrength,
chargeLimitEnabled = prefs.chargeLimitEnabled,
videoOutputOverrideEnabled = prefs.videoOutputOverrideEnabled,
)
}
}
Expand Down Expand Up @@ -219,6 +221,41 @@ class MainViewModel @Inject constructor(
}
}

fun updateVideoOutputOverridePreference(newValue: Boolean) {
prefs.videoOutputOverrideEnabled = newValue
_uiState.update {
it.copy(videoOutputOverrideEnabled = newValue)
}
}

fun videoOutputOverrideClicked() {
_uiState.update {
it.copy(
showVideoOutputOverrideDialog = true,
videoOutputControllerStyle = ControllerStyle.getById(prefs.videoOutputControllerStyle),
videoOutputL2R2Style = L2R2Style.getById(prefs.videoOutputL2R2Style),
)
}
}

fun videoOutputOverrideDialogDismissed() {
_uiState.update {
it.copy(showVideoOutputOverrideDialog = false)
}
}

fun saveVideoOutputOverride(newControllerStyle: ControllerStyle, newL2R2Style: L2R2Style) {
prefs.videoOutputControllerStyle = newControllerStyle.id
prefs.videoOutputL2R2Style = newL2R2Style.id
_uiState.update {
it.copy(
showVideoOutputOverrideDialog = false,
videoOutputControllerStyle = newControllerStyle,
videoOutputL2R2Style = newL2R2Style,
)
}
}

fun appOverridesEnabled(newValue: Boolean) {
prefs.appOverridesEnabled = newValue
_uiState.update {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import de.langerhans.odintools.models.L2R2Style
import de.langerhans.odintools.models.PerfMode
import de.langerhans.odintools.tools.BatteryLevelReceiver
import de.langerhans.odintools.tools.ShellExecutor
import de.langerhans.odintools.tools.VideoOutputReceiver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand All @@ -38,6 +39,7 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService()
lateinit var prefs: SharedPrefsRepo

private var batteryLevelReceiver: BatteryLevelReceiver = BatteryLevelReceiver()
private var videoOutputReceiver: VideoOutputReceiver = VideoOutputReceiver()

private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
Expand All @@ -63,6 +65,7 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService()
}

private var chargeLimitEnabled: Boolean = false
private var videoOutputOverrideEnabled: Boolean = false

override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (shouldIgnore(event)) return
Expand Down Expand Up @@ -102,18 +105,21 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService()
savedFanMode = FanMode.getMode(executor)
}

ControllerStyle.getById(override.controllerStyle).takeIf {
it != Unknown
}?.enable(executor) ?: run {
// Reset to default if we switch between override and NoChange app
savedControllerStyle?.enable(executor)
}
// Avoid conflicts with Video Output Override
if (!videoOutputOverrideEnabled || !videoOutputReceiver.overrideEnabled) {
ControllerStyle.getById(override.controllerStyle).takeIf {
it != Unknown
}?.enable(executor) ?: run {
// Reset to default if we switch between override and NoChange app
savedControllerStyle?.enable(executor)
}

L2R2Style.getById(override.l2R2Style).takeIf {
it != L2R2Style.Unknown
}?.enable(executor) ?: run {
// Reset to default if we switch between override and NoChange app
savedL2R2Style?.enable(executor)
L2R2Style.getById(override.l2R2Style).takeIf {
it != L2R2Style.Unknown
}?.enable(executor) ?: run {
// Reset to default if we switch between override and NoChange app
savedL2R2Style?.enable(executor)
}
}

PerfMode.getById(override.perfMode).takeIf {
Expand Down Expand Up @@ -165,6 +171,20 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService()
chargeLimitEnabled = newValue
}

private fun applyVideoOutputOverride(newValue: Boolean) {
if (newValue && !videoOutputOverrideEnabled) {
val intentFilter = IntentFilter().apply {
VideoOutputReceiver.ALLOWED_INTENTS.forEach { action ->
addAction(action)
}
}
registerReceiver(videoOutputReceiver, intentFilter)
} else if (!newValue && videoOutputOverrideEnabled) {
unregisterReceiver(videoOutputReceiver)
}
videoOutputOverrideEnabled = newValue
}

override fun onInterrupt() {
// Nothing here
}
Expand Down Expand Up @@ -192,6 +212,11 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService()
applyChargeLimit(it)
}

applyVideoOutputOverride(prefs.videoOutputOverrideEnabled)
prefs.observeVideoOutputOverrideEnabledState {
applyVideoOutputOverride(it)
}

scope.launch {
appOverrideDao.getAll()
.flowOn(Dispatchers.IO)
Expand All @@ -207,6 +232,7 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService()
contentResolver.unregisterContentObserver(imeObserver)
prefs.removeAppOverrideEnabledObserver()
prefs.removeChargeLimitEnabledObserver()
prefs.removeVideoOutputOverrideEnabledObserver()
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package de.langerhans.odintools.tools

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import dagger.hilt.android.AndroidEntryPoint
import de.langerhans.odintools.data.SharedPrefsRepo
import de.langerhans.odintools.models.ControllerStyle
import de.langerhans.odintools.models.L2R2Style
import de.langerhans.odintools.service.ForegroundAppWatcherService.Companion.OVERRIDE_DELAY
import javax.inject.Inject

@AndroidEntryPoint
class VideoOutputReceiver : BroadcastReceiver() {

var overrideEnabled = false

private var savedControllerStyle: ControllerStyle? = null
private var savedL2R2Style: L2R2Style? = null

@Inject
lateinit var executor: ShellExecutor

@Inject
lateinit var prefs: SharedPrefsRepo

private fun handleEvent(connected: Boolean?) {
if (connected == true && !overrideEnabled) {
// Save default styles
savedControllerStyle = ControllerStyle.getStyle(executor)
savedL2R2Style = L2R2Style.getStyle(executor)

// Apply style profile
ControllerStyle.getById(prefs.videoOutputControllerStyle).takeIf {
it != ControllerStyle.Unknown
}?.enable(executor)
L2R2Style.getById(prefs.videoOutputL2R2Style).takeIf {
it != L2R2Style.Unknown
}?.enable(executor)

overrideEnabled = true
} else if (connected == false && overrideEnabled) {
// Reset to defaults
savedControllerStyle?.enable(executor)
savedL2R2Style?.enable(executor)

overrideEnabled = false
}
}

override fun onReceive(context: Context, intent: Intent) {
if (intent.action !in ALLOWED_INTENTS) {
return
}

val connected = intent.extras?.getBoolean("is_connected")

if (prefs.overrideDelay) {
Handler(Looper.getMainLooper()).postDelayed({ handleEvent(connected) }, OVERRIDE_DELAY)
} else {
handleEvent(connected)
}
}

companion object {
private const val ACTION_DP_STATUS_CHANGED = "com.retrostation.action.DP_STATUS_CHANGED"
private const val ACTION_HDMI_STATUS_CHANGED = "com.retrostation.action.HDMI_STATUS_CHANGED"
private const val ACTION_HDMI_OR_DP_STATUS_CHANGED = "com.retrostation.action.HDMI_OR_DP_STATUS_CHANGED"
val ALLOWED_INTENTS = listOf(
ACTION_DP_STATUS_CHANGED,
ACTION_HDMI_STATUS_CHANGED,
ACTION_HDMI_OR_DP_STATUS_CHANGED,
)
}
}
Loading