diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..07764a7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6473b8..42268b9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,9 @@ android { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + hilt { + enableAggregatingTask = true + } } room { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6e68d4b..031adee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,73 +1,81 @@ - + - - + + + + + android:name=".OdinToolsApplication" + android:icon="@mipmap/ic_launcher" + android:label="@string/appName" + android:supportsRtl="true" + android:theme="@style/Theme.OdinTools" + android:dataExtractionRules="@xml/data_extraction_rules"> + android:name=".main.MainActivity" + android:exported="true" + android:theme="@style/Theme.OdinTools" + android:screenOrientation="landscape"> - + - + + android:name=".tiles.ControllerTileService" + android:exported="true" + android:icon="@drawable/ic_face_buttons" + android:label="@string/controllerStyle" + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> - + + android:name="android.service.quicksettings.TOGGLEABLE_TILE" + android:value="true" /> + android:name=".tiles.L2R2TileService" + android:exported="true" + android:icon="@drawable/ic_sliders" + android:label="@string/l2r2mode" + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> - + + android:name="android.service.quicksettings.TOGGLEABLE_TILE" + android:value="true" /> - + - + - + + + android:name=".tools.BootReceiver" + android:enabled="true" + android:exported="true"> - + - \ No newline at end of file + diff --git a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListScreen.kt b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListScreen.kt index 9d0fe26..fade0f0 100644 --- a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListScreen.kt +++ b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListScreen.kt @@ -3,10 +3,19 @@ package de.langerhans.odintools.appsettings import android.graphics.drawable.Drawable import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue diff --git a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListViewModel.kt b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListViewModel.kt index acf9405..83e40a4 100644 --- a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListViewModel.kt +++ b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverrideListViewModel.kt @@ -6,7 +6,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import de.langerhans.odintools.data.AppOverrideDao import de.langerhans.odintools.tools.DeviceUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesScreen.kt b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesScreen.kt index 9faee47..689b94a 100644 --- a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesScreen.kt +++ b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesScreen.kt @@ -2,11 +2,29 @@ package de.langerhans.odintools.appsettings import androidx.annotation.StringRes import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -16,12 +34,21 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.drawablepainter.rememberDrawablePainter import de.langerhans.odintools.R -import de.langerhans.odintools.models.ControllerStyle.* -import de.langerhans.odintools.models.FanMode.* -import de.langerhans.odintools.models.L2R2Style.* +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.FanMode.Off +import de.langerhans.odintools.models.FanMode.Quiet +import de.langerhans.odintools.models.FanMode.Smart +import de.langerhans.odintools.models.FanMode.Sport +import de.langerhans.odintools.models.L2R2Style.Analog +import de.langerhans.odintools.models.L2R2Style.Both +import de.langerhans.odintools.models.L2R2Style.Digital import de.langerhans.odintools.models.NoChange import de.langerhans.odintools.models.PerfMode -import de.langerhans.odintools.models.PerfMode.* +import de.langerhans.odintools.models.PerfMode.HighPerformance +import de.langerhans.odintools.models.PerfMode.Performance +import de.langerhans.odintools.models.PerfMode.Standard import de.langerhans.odintools.ui.composables.DeleteConfirmDialog import de.langerhans.odintools.ui.composables.LargeDropdownMenu import de.langerhans.odintools.ui.composables.OdinTopAppBar diff --git a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesViewModel.kt b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesViewModel.kt index f280fa2..6751dfb 100644 --- a/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesViewModel.kt +++ b/app/src/main/java/de/langerhans/odintools/appsettings/AppOverridesViewModel.kt @@ -6,8 +6,12 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.langerhans.odintools.data.AppOverrideDao import de.langerhans.odintools.data.AppOverrideEntity -import de.langerhans.odintools.models.* +import de.langerhans.odintools.models.ControllerStyle +import de.langerhans.odintools.models.FanMode import de.langerhans.odintools.models.FanMode.Companion.getDisabledFanModes +import de.langerhans.odintools.models.L2R2Style +import de.langerhans.odintools.models.NoChange +import de.langerhans.odintools.models.PerfMode import de.langerhans.odintools.tools.DeviceUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/de/langerhans/odintools/data/AppOverrideDao.kt b/app/src/main/java/de/langerhans/odintools/data/AppOverrideDao.kt index 3dc1520..ea77f83 100644 --- a/app/src/main/java/de/langerhans/odintools/data/AppOverrideDao.kt +++ b/app/src/main/java/de/langerhans/odintools/data/AppOverrideDao.kt @@ -1,6 +1,9 @@ package de.langerhans.odintools.data -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query import kotlinx.coroutines.flow.Flow @Dao diff --git a/app/src/main/java/de/langerhans/odintools/data/SharedPrefsRepo.kt b/app/src/main/java/de/langerhans/odintools/data/SharedPrefsRepo.kt index 14773c4..685e3da 100644 --- a/app/src/main/java/de/langerhans/odintools/data/SharedPrefsRepo.kt +++ b/app/src/main/java/de/langerhans/odintools/data/SharedPrefsRepo.kt @@ -6,7 +6,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject class SharedPrefsRepo @Inject constructor( - @ApplicationContext context: Context, + @ApplicationContext private val context: Context, ) { private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -35,14 +35,45 @@ class SharedPrefsRepo @Inject constructor( get() = prefs.getBoolean(KEY_OVERRIDE_DELAY, false) set(value) = prefs.edit().putBoolean(KEY_OVERRIDE_DELAY, value).apply() + var chargeLimitEnabled + get() = prefs.getBoolean(KEY_CHARGE_LIMIT_ENABLED, false) + set(value) = prefs.edit().putBoolean(KEY_CHARGE_LIMIT_ENABLED, value).apply() + + var minBatteryLevel + get() = prefs.getInt(KEY_MIN_BATTERY_LEVEL, 20) + set(value) = prefs.edit().putInt(KEY_MIN_BATTERY_LEVEL, value).apply() + + var maxBatteryLevel + get() = prefs.getInt(KEY_MAX_BATTERY_LEVEL, 80) + set(value) = prefs.edit().putInt(KEY_MAX_BATTERY_LEVEL, value).apply() + + private var chargeLimitEnabledListener: OnSharedPreferenceChangeListener? = null + + fun observeChargeLimitEnabledState(onChargeLimitEnabled: (newState: Boolean) -> Unit) { + chargeLimitEnabledListener = OnSharedPreferenceChangeListener { _, key -> + if (key == KEY_CHARGE_LIMIT_ENABLED) { + onChargeLimitEnabled(chargeLimitEnabled) + } + } + prefs.registerOnSharedPreferenceChangeListener(chargeLimitEnabledListener) + } + + fun removeChargeLimitEnabledObserver() { + prefs.unregisterOnSharedPreferenceChangeListener(chargeLimitEnabledListener) + chargeLimitEnabledListener = null + } + private var appOverrideEnabledListener: OnSharedPreferenceChangeListener? = null - fun observeAppOverrideEnabledState(overridesEnabled: (newState: Boolean) -> Unit, overrideDelayEnabled: (newState: Boolean) -> Unit) { + fun observeAppOverrideEnabledState( + onAppOverridesEnabled: (newState: Boolean) -> Unit, + onOverrideDelayEnabled: (newState: Boolean) -> Unit, + ) { appOverrideEnabledListener = OnSharedPreferenceChangeListener { _, key -> if (key == KEY_APP_OVERRIDE_ENABLED) { - overridesEnabled(appOverridesEnabled) + onAppOverridesEnabled(appOverridesEnabled) } else if (key == KEY_OVERRIDE_DELAY) { - overrideDelayEnabled(overrideDelay) + onOverrideDelayEnabled(overrideDelay) } } prefs.registerOnSharedPreferenceChangeListener(appOverrideEnabledListener) @@ -62,5 +93,8 @@ class SharedPrefsRepo @Inject constructor( private const val KEY_VIBRATION_STRENGTH = "vibration_strength" private const val KEY_APP_OVERRIDE_ENABLED = "app_override_enabled" private const val KEY_OVERRIDE_DELAY = "override_delay" + 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" } } diff --git a/app/src/main/java/de/langerhans/odintools/main/MainActivity.kt b/app/src/main/java/de/langerhans/odintools/main/MainActivity.kt index 8a260a0..ff24b77 100644 --- a/app/src/main/java/de/langerhans/odintools/main/MainActivity.kt +++ b/app/src/main/java/de/langerhans/odintools/main/MainActivity.kt @@ -30,12 +30,23 @@ import de.langerhans.odintools.R import de.langerhans.odintools.appsettings.AppOverrideListScreen import de.langerhans.odintools.appsettings.AppOverridesScreen import de.langerhans.odintools.tools.DeviceType.ODIN2 -import de.langerhans.odintools.ui.composables.* +import de.langerhans.odintools.tools.SettingsRepo +import de.langerhans.odintools.ui.composables.ChargeLimitPreferenceDialog +import de.langerhans.odintools.ui.composables.CheckBoxDialogPreference +import de.langerhans.odintools.ui.composables.NotAnOdinDialog +import de.langerhans.odintools.ui.composables.OdinTopAppBar +import de.langerhans.odintools.ui.composables.PServerNotAvailableDialog +import de.langerhans.odintools.ui.composables.RemapButtonDialog +import de.langerhans.odintools.ui.composables.SaturationPreferenceDialog +import de.langerhans.odintools.ui.composables.SettingsHeader +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.theme.OdinToolsTheme @AndroidEntryPoint class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -68,13 +79,14 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid if (uiState.showPServerNotAvailableDialog) { PServerNotAvailableDialog() - } else if (uiState.showNotAnOdinDialog) { + } else if (uiState.showIncompatibleDeviceDialog) { NotAnOdinDialog { viewModel.incompatibleDeviceDialogDismissed() } } if (uiState.showControllerStyleDialog) { CheckBoxDialogPreference( items = viewModel.controllerStyleOptions, + title = R.string.controllerStyle, onCancel = { viewModel.hideControllerStylePreference() }, @@ -87,6 +99,7 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid if (uiState.showL2r2StyleDialog) { CheckBoxDialogPreference( items = viewModel.l2r2StyleOptions, + title = R.string.l2r2mode, onCancel = { viewModel.hideL2r2StylePreference() }, @@ -121,6 +134,14 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid ) } + if (uiState.showChargeLimitDialog) { + ChargeLimitPreferenceDialog( + initialValue = uiState.currentChargeLimit, + onCancel = { viewModel.chargeLimitDialogDismissed() }, + onSave = { viewModel.saveChargeLimit(it) }, + ) + } + Scaffold(topBar = { OdinTopAppBar(deviceVersion = uiState.deviceVersion) }) { contentPadding -> Column( modifier = Modifier @@ -141,14 +162,14 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid SwitchPreference( icon = R.drawable.ic_more_time, title = R.string.overrideDelay, - description = R.string.overrideDelayDesc, + description = R.string.overrideDelayDescription, state = uiState.overrideDelayEnabled, ) { viewModel.overrideDelayEnabled(it) } - SettingsHeader(R.string.quicksettings) + SettingsHeader(R.string.quickSettings) TriggerPreference( - icon = R.drawable.ic_controllerstyle, + icon = R.drawable.ic_face_buttons, title = R.string.controllerStyle, description = R.string.controllerStyleDesc, ) { @@ -164,11 +185,11 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid SettingsHeader(R.string.buttons) SwitchPreference( icon = R.drawable.ic_home, - title = R.string.doubleHomeTitle, - description = R.string.doubleHomeDescription, - state = uiState.singleHomeEnabled, + title = R.string.singlePressHome, + description = R.string.singlePressHomeDescription, + state = uiState.singlePressHomeEnabled, ) { - viewModel.updateSingleHomePreference(it) + viewModel.updateSinglePressHomePreference(it) } if (uiState.deviceType == ODIN2) { TriggerPreference( @@ -176,14 +197,14 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid title = R.string.m1Button, description = R.string.remapButtonDescription, ) { - viewModel.remapButtonClicked("remap_custom_to_m1_value") + viewModel.remapButtonClicked(SettingsRepo.KEY_CUSTOM_M1_VALUE) } TriggerPreference( icon = R.drawable.ic_gamepad, title = R.string.m2Button, description = R.string.remapButtonDescription, ) { - viewModel.remapButtonClicked("remap_custom_to_m2_value") + viewModel.remapButtonClicked(SettingsRepo.KEY_CUSTOM_M2_VALUE) } } SettingsHeader(name = R.string.display) @@ -205,6 +226,24 @@ fun SettingsScreen(viewModel: MainViewModel = hiltViewModel(), navigateToOverrid ) { viewModel.updateVibrationPreference(it) } + SettingsHeader(R.string.battery) + SwitchableTriggerPreference( + icon = R.drawable.ic_electrical_services, + title = R.string.chargeLimit, + description = R.string.chargeLimitDescription, + state = uiState.chargeLimitEnabled, + onClick = { viewModel.chargeLimitClicked() }, + ) { + viewModel.updateChargeLimitPreference(it) + } + } + SettingsHeader(R.string.debug) + TriggerPreference( + icon = R.drawable.ic_file_save, + title = R.string.dumpLogToFile, + description = R.string.dumpLogToFileDescription, + ) { + viewModel.dumpLogToFile() } // Navigation bar padding diff --git a/app/src/main/java/de/langerhans/odintools/main/MainUiModel.kt b/app/src/main/java/de/langerhans/odintools/main/MainUiModel.kt index 76c6e08..b487896 100644 --- a/app/src/main/java/de/langerhans/odintools/main/MainUiModel.kt +++ b/app/src/main/java/de/langerhans/odintools/main/MainUiModel.kt @@ -10,10 +10,10 @@ import de.langerhans.odintools.tools.DeviceType.ODIN2 data class MainUiModel( val deviceType: DeviceType = ODIN2, val deviceVersion: String = "", - val showNotAnOdinDialog: Boolean = false, + val showIncompatibleDeviceDialog: Boolean = false, val showPServerNotAvailableDialog: Boolean = false, - val singleHomeEnabled: Boolean = false, + val singlePressHomeEnabled: Boolean = false, val appOverridesEnabled: Boolean = true, val overrideDelayEnabled: Boolean = false, @@ -30,6 +30,10 @@ data class MainUiModel( val showRemapButtonDialog: Boolean = false, val currentButtonSetting: String = "", val currentButtonKeyCode: Int = 0, + + val showChargeLimitDialog: Boolean = false, + val chargeLimitEnabled: Boolean = false, + val currentChargeLimit: ClosedRange = 20..80, ) class CheckboxPreferenceUiModel( diff --git a/app/src/main/java/de/langerhans/odintools/main/MainViewModel.kt b/app/src/main/java/de/langerhans/odintools/main/MainViewModel.kt index 333b4f4..a6bc2aa 100644 --- a/app/src/main/java/de/langerhans/odintools/main/MainViewModel.kt +++ b/app/src/main/java/de/langerhans/odintools/main/MainViewModel.kt @@ -6,21 +6,30 @@ 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.L2R2Style.* +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.Analog +import de.langerhans.odintools.models.L2R2Style.Both +import de.langerhans.odintools.models.L2R2Style.Digital import de.langerhans.odintools.tools.DeviceType.ODIN2 import de.langerhans.odintools.tools.DeviceUtils +import de.langerhans.odintools.tools.SettingsRepo import de.langerhans.odintools.tools.ShellExecutor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - deviceUtils: DeviceUtils, + private val deviceUtils: DeviceUtils, private val executor: ShellExecutor, + private val settings: SettingsRepo, private val prefs: SharedPrefsRepo, ) : ViewModel() { @@ -36,39 +45,37 @@ class MainViewModel @Inject constructor( get() = _l2r2StyleOptions init { - executor.applyRequiredSettings() + settings.applyRequiredSettings() val deviceType = deviceUtils.getDeviceType() - val preventHomePressSetting = executor.getBooleanSystemSetting("prevent_press_home_accidentally", true) - val vibrationOnSetting = executor.getBooleanSystemSetting("vibrate_on", true) - val vibrationStrength = executor.getVibrationStrength() _uiState.update { _ -> MainUiModel( deviceType = deviceType, deviceVersion = deviceUtils.getDeviceVersion(), - showNotAnOdinDialog = deviceType != ODIN2, - singleHomeEnabled = !preventHomePressSetting, + showIncompatibleDeviceDialog = deviceType != ODIN2, + singlePressHomeEnabled = !settings.preventPressHome, showPServerNotAvailableDialog = !executor.pServerAvailable, overrideDelayEnabled = prefs.overrideDelay, - vibrationEnabled = vibrationOnSetting, - currentVibration = vibrationStrength, + vibrationEnabled = settings.vibrationEnabled, + currentVibration = settings.vibrationStrength, + chargeLimitEnabled = prefs.chargeLimitEnabled, ) } } fun incompatibleDeviceDialogDismissed() { _uiState.update { current -> - current.copy(showNotAnOdinDialog = false) + current.copy(showIncompatibleDeviceDialog = false) } } - fun updateSingleHomePreference(newValue: Boolean) { + fun updateSinglePressHomePreference(newValue: Boolean) { // Invert here as prevent == double press - executor.setBooleanSystemSetting("prevent_press_home_accidentally", !newValue) + settings.preventPressHome = !newValue _uiState.update { current -> - current.copy(singleHomeEnabled = newValue) + current.copy(singlePressHomeEnabled = newValue) } } @@ -100,14 +107,14 @@ class MainViewModel @Inject constructor( fun showL2r2StylePreference() { _l2r2StyleOptions = getCurrentL2r2Styles().toMutableStateList() - _uiState.update { current -> - current.copy(showL2r2StyleDialog = true) + _uiState.update { + it.copy(showL2r2StyleDialog = true) } } fun hideL2r2StylePreference() { - _uiState.update { current -> - current.copy(showL2r2StyleDialog = false) + _uiState.update { + it.copy(showL2r2StyleDialog = false) } } @@ -138,23 +145,22 @@ class MainViewModel @Inject constructor( fun saveSaturation(newValue: Float) { prefs.saturationOverride = newValue - executor.setSfSaturation(newValue) + settings.setSfSaturation(newValue) _uiState.update { it.copy(showSaturationDialog = false) } } fun updateVibrationPreference(newValue: Boolean) { - executor.setBooleanSystemSetting("vibrate_on", newValue) - - _uiState.update { current -> - current.copy(vibrationEnabled = newValue) + settings.vibrationEnabled = newValue + _uiState.update { + it.copy(vibrationEnabled = newValue) } } fun vibrationClicked() { _uiState.update { - it.copy(showVibrationDialog = true, currentVibration = executor.getVibrationStrength()) + it.copy(showVibrationDialog = true, currentVibration = settings.vibrationStrength) } } @@ -166,7 +172,7 @@ class MainViewModel @Inject constructor( fun saveVibration(newValue: Int) { prefs.vibrationStrength = newValue - executor.setVibrationStrength(newValue) + settings.vibrationStrength = newValue _uiState.update { it.copy(showVibrationDialog = false, currentVibration = newValue) } @@ -189,10 +195,10 @@ class MainViewModel @Inject constructor( } private fun getDefaultKeyCode(setting: String): Int { - if (setting == "remap_custom_to_m1_value") { + if (setting == SettingsRepo.KEY_CUSTOM_M1_VALUE) { return KeyEvent.KEYCODE_BUTTON_C } - if (setting == "remap_custom_to_m2_value") { + if (setting == SettingsRepo.KEY_CUSTOM_M2_VALUE) { return KeyEvent.KEYCODE_BUTTON_Z } return KeyEvent.KEYCODE_UNKNOWN @@ -226,4 +232,42 @@ class MainViewModel @Inject constructor( it.copy(overrideDelayEnabled = newValue) } } + + fun updateChargeLimitPreference(newValue: Boolean) { + prefs.chargeLimitEnabled = newValue + _uiState.update { + it.copy(chargeLimitEnabled = newValue) + } + } + + fun chargeLimitClicked() { + _uiState.update { + it.copy(showChargeLimitDialog = true, currentChargeLimit = prefs.minBatteryLevel..prefs.maxBatteryLevel) + } + } + + fun chargeLimitDialogDismissed() { + _uiState.update { + it.copy(showChargeLimitDialog = false) + } + } + + fun saveChargeLimit(newValue: ClosedRange) { + prefs.minBatteryLevel = newValue.start + prefs.maxBatteryLevel = newValue.endInclusive + + _uiState.update { + it.copy(showChargeLimitDialog = false, currentChargeLimit = newValue) + } + } + + fun dumpLogToFile() { + val currentDate = Date() + val dateFormat = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()) + val timeStamp = dateFormat.format(currentDate) + val directory = "/storage/emulated/0" + val fileName = "$directory/OdinTools_$timeStamp.log" + executor + .executeAsRoot("logcat -d -v threadtime > $fileName") + } } diff --git a/app/src/main/java/de/langerhans/odintools/models/PerfMode.kt b/app/src/main/java/de/langerhans/odintools/models/PerfMode.kt index 5ee7106..1d75646 100644 --- a/app/src/main/java/de/langerhans/odintools/models/PerfMode.kt +++ b/app/src/main/java/de/langerhans/odintools/models/PerfMode.kt @@ -11,7 +11,7 @@ sealed class PerfMode( ) { data object Standard : PerfMode("standard", 0, R.string.standard) data object Performance : PerfMode("performance", 1, R.string.performance) - data object HighPerformance : PerfMode("highPerformance", 2, R.string.highperformance) + data object HighPerformance : PerfMode("highPerformance", 2, R.string.highPerformance) data object Unknown : PerfMode("unknown", -1, R.string.unknown) fun enable(executor: ShellExecutor) { diff --git a/app/src/main/java/de/langerhans/odintools/service/ForegroundAppWatcherService.kt b/app/src/main/java/de/langerhans/odintools/service/ForegroundAppWatcherService.kt index 84b73b7..1225c3d 100644 --- a/app/src/main/java/de/langerhans/odintools/service/ForegroundAppWatcherService.kt +++ b/app/src/main/java/de/langerhans/odintools/service/ForegroundAppWatcherService.kt @@ -1,6 +1,7 @@ package de.langerhans.odintools.service import android.accessibilityservice.AccessibilityService +import android.content.IntentFilter import android.database.ContentObserver import android.os.Handler import android.os.Looper @@ -15,6 +16,7 @@ import de.langerhans.odintools.models.ControllerStyle.Unknown import de.langerhans.odintools.models.FanMode 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -30,10 +32,12 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService() lateinit var appOverrideDao: AppOverrideDao @Inject - lateinit var shellExecutor: ShellExecutor + lateinit var executor: ShellExecutor @Inject - lateinit var sharedPrefsRepo: SharedPrefsRepo + lateinit var prefs: SharedPrefsRepo + + private var batteryLevelReceiver: BatteryLevelReceiver = BatteryLevelReceiver() private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) @@ -52,12 +56,14 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService() private var currentIme = "" private val imeObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean) { - currentIme = shellExecutor + currentIme = executor .executeAsRoot("settings get secure default_input_method") .getOrDefault("") ?: "" } } + private var chargeLimitEnabled: Boolean = false + override fun onAccessibilityEvent(event: AccessibilityEvent) { if (shouldIgnore(event)) return @@ -90,38 +96,38 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService() private fun applyOverride(override: AppOverrideEntity) { // This check makes sure that we don't override the "defaults" when switching between apps with overrides if (!hasSetOverride) { - savedControllerStyle = ControllerStyle.getStyle(shellExecutor) - savedL2R2Style = L2R2Style.getStyle(shellExecutor) - savedPerfMode = PerfMode.getMode(shellExecutor) - savedFanMode = FanMode.getMode(shellExecutor) + savedControllerStyle = ControllerStyle.getStyle(executor) + savedL2R2Style = L2R2Style.getStyle(executor) + savedPerfMode = PerfMode.getMode(executor) + savedFanMode = FanMode.getMode(executor) } ControllerStyle.getById(override.controllerStyle).takeIf { it != Unknown - }?.enable(shellExecutor) ?: run { + }?.enable(executor) ?: run { // Reset to default if we switch between override and NoChange app - savedControllerStyle?.enable(shellExecutor) + savedControllerStyle?.enable(executor) } L2R2Style.getById(override.l2R2Style).takeIf { it != L2R2Style.Unknown - }?.enable(shellExecutor) ?: run { + }?.enable(executor) ?: run { // Reset to default if we switch between override and NoChange app - savedL2R2Style?.enable(shellExecutor) + savedL2R2Style?.enable(executor) } PerfMode.getById(override.perfMode).takeIf { it != PerfMode.Unknown - }?.enable(shellExecutor) ?: run { + }?.enable(executor) ?: run { // Reset to default if we switch between override and NoChange app - savedPerfMode?.enable(shellExecutor) + savedPerfMode?.enable(executor) } FanMode.getById(override.fanMode).takeIf { it != FanMode.Unknown - }?.enable(shellExecutor) ?: run { + }?.enable(executor) ?: run { // Reset to default if we switch between override and NoChange app - savedFanMode?.enable(shellExecutor) + savedFanMode?.enable(executor) } hasSetOverride = true @@ -130,21 +136,35 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService() private fun resetOverrides() { if (!hasSetOverride) return - savedControllerStyle?.enable(shellExecutor) + savedControllerStyle?.enable(executor) savedControllerStyle = null - savedL2R2Style?.enable(shellExecutor) + savedL2R2Style?.enable(executor) savedL2R2Style = null - savedPerfMode?.enable(shellExecutor) + savedPerfMode?.enable(executor) savedPerfMode = null - savedFanMode?.enable(shellExecutor) + savedFanMode?.enable(executor) savedFanMode = null hasSetOverride = false } + private fun applyChargeLimit(newValue: Boolean) { + if (newValue && !chargeLimitEnabled) { + val intentFilter = IntentFilter().apply { + BatteryLevelReceiver.ALLOWED_INTENTS.forEach { action -> + addAction(action) + } + } + registerReceiver(batteryLevelReceiver, intentFilter) + } else if (!newValue && chargeLimitEnabled) { + unregisterReceiver(batteryLevelReceiver) + } + chargeLimitEnabled = newValue + } + override fun onInterrupt() { // Nothing here } @@ -152,7 +172,7 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService() override fun onServiceConnected() { super.onServiceConnected() - currentIme = shellExecutor + currentIme = executor .executeAsRoot("settings get secure default_input_method") .getOrDefault("") ?: "" contentResolver.registerContentObserver( @@ -161,12 +181,17 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService() imeObserver, ) - overridesEnabled = sharedPrefsRepo.appOverridesEnabled - sharedPrefsRepo.observeAppOverrideEnabledState( + overridesEnabled = prefs.appOverridesEnabled + prefs.observeAppOverrideEnabledState( { overridesEnabled = it }, { overridesDelay = it }, ) + applyChargeLimit(prefs.chargeLimitEnabled) + prefs.observeChargeLimitEnabledState { + applyChargeLimit(it) + } + scope.launch { appOverrideDao.getAll() .flowOn(Dispatchers.IO) @@ -180,7 +205,8 @@ class ForegroundAppWatcherService @Inject constructor() : AccessibilityService() super.onDestroy() job.cancel() contentResolver.unregisterContentObserver(imeObserver) - sharedPrefsRepo.removeAppOverrideEnabledObserver() + prefs.removeAppOverrideEnabledObserver() + prefs.removeChargeLimitEnabledObserver() } companion object { diff --git a/app/src/main/java/de/langerhans/odintools/tiles/ControllerTileService.kt b/app/src/main/java/de/langerhans/odintools/tiles/ControllerTileService.kt index 1efefa8..b272bf9 100644 --- a/app/src/main/java/de/langerhans/odintools/tiles/ControllerTileService.kt +++ b/app/src/main/java/de/langerhans/odintools/tiles/ControllerTileService.kt @@ -6,7 +6,10 @@ import dagger.hilt.android.AndroidEntryPoint import de.langerhans.odintools.R import de.langerhans.odintools.data.SharedPrefsRepo import de.langerhans.odintools.models.ControllerStyle -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.Unknown +import de.langerhans.odintools.models.ControllerStyle.Xbox import de.langerhans.odintools.tools.ShellExecutor import javax.inject.Inject diff --git a/app/src/main/java/de/langerhans/odintools/tiles/L2R2TileService.kt b/app/src/main/java/de/langerhans/odintools/tiles/L2R2TileService.kt index 3e3199d..bb5883c 100644 --- a/app/src/main/java/de/langerhans/odintools/tiles/L2R2TileService.kt +++ b/app/src/main/java/de/langerhans/odintools/tiles/L2R2TileService.kt @@ -6,7 +6,10 @@ import dagger.hilt.android.AndroidEntryPoint import de.langerhans.odintools.R import de.langerhans.odintools.data.SharedPrefsRepo import de.langerhans.odintools.models.L2R2Style -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 +import de.langerhans.odintools.models.L2R2Style.Unknown import de.langerhans.odintools.tools.ShellExecutor import javax.inject.Inject diff --git a/app/src/main/java/de/langerhans/odintools/tools/BatteryLevelReceiver.kt b/app/src/main/java/de/langerhans/odintools/tools/BatteryLevelReceiver.kt new file mode 100644 index 0000000..d40ab93 --- /dev/null +++ b/app/src/main/java/de/langerhans/odintools/tools/BatteryLevelReceiver.kt @@ -0,0 +1,49 @@ +package de.langerhans.odintools.tools + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Context.BATTERY_SERVICE +import android.content.Intent +import android.os.BatteryManager +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.qualifiers.ApplicationContext +import de.langerhans.odintools.data.SharedPrefsRepo +import javax.inject.Inject + +@AndroidEntryPoint +class BatteryLevelReceiver : BroadcastReceiver() { + + @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var settings: SettingsRepo + + @Inject + lateinit var prefs: SharedPrefsRepo + + override fun onReceive(context: Context, intent: Intent) { + if (!prefs.chargeLimitEnabled || intent.action !in ALLOWED_INTENTS) { + return + } + + val batteryManager = context.getSystemService(BATTERY_SERVICE) as BatteryManager + val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + + if (batteryLevel >= prefs.maxBatteryLevel && !settings.chargingSeparationEnabled()) { + settings.enableChargingSeparation() + } else if (batteryLevel <= prefs.minBatteryLevel && settings.chargingSeparationEnabled()) { + settings.disableChargingSeparation() + } + } + + companion object { + val ALLOWED_INTENTS = listOf( + Intent.ACTION_BATTERY_CHANGED, + Intent.ACTION_POWER_CONNECTED, + Intent.ACTION_POWER_DISCONNECTED, + Intent.ACTION_SCREEN_OFF, + Intent.ACTION_SCREEN_ON, + ) + } +} diff --git a/app/src/main/java/de/langerhans/odintools/tools/BootReceiver.kt b/app/src/main/java/de/langerhans/odintools/tools/BootReceiver.kt index 8dbe6fb..0872a25 100644 --- a/app/src/main/java/de/langerhans/odintools/tools/BootReceiver.kt +++ b/app/src/main/java/de/langerhans/odintools/tools/BootReceiver.kt @@ -11,23 +11,22 @@ import javax.inject.Inject class BootReceiver : BroadcastReceiver() { @Inject - lateinit var sharedPrefsRepo: SharedPrefsRepo + lateinit var settings: SettingsRepo @Inject - lateinit var executor: ShellExecutor + lateinit var prefs: SharedPrefsRepo override fun onReceive(context: Context, intent: Intent) { if (intent.action != Intent.ACTION_BOOT_COMPLETED) { return } - - val saturation = sharedPrefsRepo.saturationOverride + val saturation = prefs.saturationOverride if (saturation != 1.0f) { - executor.setSfSaturation(saturation) + settings.setSfSaturation(saturation) } - val vibrationStrength = sharedPrefsRepo.vibrationStrength + val vibrationStrength = prefs.vibrationStrength if (vibrationStrength != 0) { - executor.setVibrationStrength(vibrationStrength) + settings.vibrationStrength = vibrationStrength } } } diff --git a/app/src/main/java/de/langerhans/odintools/tools/DeviceUtils.kt b/app/src/main/java/de/langerhans/odintools/tools/DeviceUtils.kt index e61c07c..56cb247 100644 --- a/app/src/main/java/de/langerhans/odintools/tools/DeviceUtils.kt +++ b/app/src/main/java/de/langerhans/odintools/tools/DeviceUtils.kt @@ -1,23 +1,25 @@ package de.langerhans.odintools.tools -import de.langerhans.odintools.tools.DeviceType.* +import de.langerhans.odintools.tools.DeviceType.ODIN2 +import de.langerhans.odintools.tools.DeviceType.OTHER +import de.langerhans.odintools.tools.DeviceType.RP4 import javax.inject.Inject class DeviceUtils @Inject constructor( - private val shellExecutor: ShellExecutor, + private val executor: ShellExecutor, ) { + fun getDeviceVersion() = executor.getStringProperty(SettingsRepo.KEY_BUILD_VERSION, "") + + fun getDeviceCodename() = executor.getStringProperty(SettingsRepo.KEY_VENDOR_NAME, "") + fun getDeviceType(): DeviceType { - val deviceName = shellExecutor.executeAsRoot("getprop ro.vendor.retro.name").getOrNull() - return when (deviceName) { + return when (getDeviceCodename()) { "Q9" -> ODIN2 "4.0", "4.0P" -> RP4 else -> OTHER } } - - fun getDeviceVersion() = shellExecutor.executeAsRoot("getprop ro.build.odin2.ota.version").map { it ?: "" } - .getOrDefault("") } enum class DeviceType { diff --git a/app/src/main/java/de/langerhans/odintools/tools/SettingsRepo.kt b/app/src/main/java/de/langerhans/odintools/tools/SettingsRepo.kt new file mode 100644 index 0000000..af0a572 --- /dev/null +++ b/app/src/main/java/de/langerhans/odintools/tools/SettingsRepo.kt @@ -0,0 +1,118 @@ +package de.langerhans.odintools.tools + +import de.langerhans.odintools.BuildConfig +import javax.inject.Inject + +class SettingsRepo @Inject constructor( + private val executor: ShellExecutor, +) { + + fun applyRequiredSettings() { + enableA11yService() + grantAllAppsPermission() + // Don't add to whitelist on debug builds, otherwise even Android Studio can't kill the app + if (!BuildConfig.DEBUG) { + addOdinToolsToWhitelist() + } + } + + private fun enableA11yService() { + val currentServices = + executor.executeAsRoot("settings get secure $KEY_ACCESSIBILITY_SERVICES") + .map { it ?: "" } + .getOrDefault("") + + if (currentServices.contains(PACKAGE)) return + + executor.executeAsRoot( + "settings put secure $KEY_ACCESSIBILITY_SERVICES $PACKAGE/$PACKAGE.service.ForegroundAppWatcherService:$currentServices" + .trimEnd(':'), + ) + } + + private fun grantAllAppsPermission() { + executor.executeAsRoot("pm grant $PACKAGE android.permission.QUERY_ALL_PACKAGES") + } + + private fun addOdinToolsToWhitelist() { + val currentWhitelist = whitelist + if (currentWhitelist.contains(PACKAGE)) return + val newWhitelist = "$PACKAGE,$currentWhitelist".trimEnd(',') + whitelist = newWhitelist + } + + fun setSfSaturation(value: Float) { + executor.executeAsRoot("service call SurfaceFlinger 1022 f ${String.format("%.1f", value)}") + } + + fun enableChargingSeparation() { + isChargingSeparation = true + restrictCurrent = 1000 + restrictCharge = true + } + + fun disableChargingSeparation() { + isChargingSeparation = false + restrictCurrent = 1000000 + restrictCharge = false + } + + fun chargingSeparationEnabled(): Boolean { + return isChargingSeparation && restrictCharge && restrictCurrent == 1000 + } + + private var whitelist: String + get() = executor.getStringSystemSetting(KEY_APP_WHITELIST, "") + set(value) = executor.setStringSystemSetting(KEY_APP_WHITELIST, value) + + var preventPressHome: Boolean + get() = executor.getBooleanSystemSetting(KEY_PREVENT_PRESS_HOME, true) + set(value) = executor.setBooleanSystemSetting(KEY_PREVENT_PRESS_HOME, value) + + var vibrationEnabled: Boolean + get() = executor.getBooleanSystemSetting(KEY_VIBRATE_ON, false) + set(value) = executor.setBooleanSystemSetting(KEY_VIBRATE_ON, value) + + var vibrationStrength: Int + get() = executor.getIntValue(KEY_VIBRATION_STRENGTH, 0) + set(value) = executor.setIntValue(KEY_VIBRATION_STRENGTH, value) + + var isChargingSeparation: Boolean + get() = executor.getBooleanSystemSetting(KEY_CHARGING_SEPARATION, false) + set(value) = executor.setBooleanSystemSetting(KEY_CHARGING_SEPARATION, value) + + var restrictCharge: Boolean + get() = executor.getBooleanValue(KEY_RESTRICT_CHARGE, false) + set(value) = executor.setBooleanValue(KEY_RESTRICT_CHARGE, value) + + var restrictCurrent: Int + get() = executor.getIntValue(KEY_RESTRICT_CURRENT, 0) + set(value) = executor.setIntValue(KEY_RESTRICT_CURRENT, value) + + var chargingLimit80Enabled: Boolean + get() = executor.getBooleanSystemSetting(KEY_CHARGING_LIMIT_80, false) + set(value) = executor.setBooleanSystemSetting(KEY_CHARGING_LIMIT_80, value) + + var chargingLimit10Enabled: Boolean + get() = executor.getBooleanSystemSetting(KEY_CHARGING_LIMIT_10, false) + set(value) = executor.setBooleanSystemSetting(KEY_CHARGING_LIMIT_10, value) + + companion object { + private const val PACKAGE = BuildConfig.APPLICATION_ID + const val KEY_VENDOR_NAME = "ro.vendor.retro.name" + const val KEY_BUILD_VERSION = "ro.build.odin2.ota.version" + const val KEY_SATURATION = "persist.sys.sf.color_saturation" + const val KEY_ACCESSIBILITY_SERVICES = "enabled_accessibility_services" + const val KEY_APP_WHITELIST = "app_whiteList" + const val KEY_PREVENT_PRESS_HOME = "prevent_press_home_accidentally" + const val KEY_VIBRATE_ON = "vibrate_on" + const val KEY_CUSTOM_M1_VALUE = "remap_custom_to_m1_value" + const val KEY_CUSTOM_M2_VALUE = "remap_custom_to_m2_value" + const val KEY_VIBRATION_STRENGTH = "/d/haptics/user_vmax_mv" + const val KEY_CHARGING_SEPARATION = "is_charging_separation" + const val KEY_CHARGING_LIMIT_80 = "charging_limit_greater_than_80" + const val KEY_CHARGING_LIMIT_10 = "charging_limit_less_than_10" + const val KEY_RESTRICT_CHARGE = "/sys/class/qcom-battery/restrict_chg" + const val KEY_RESTRICT_CURRENT = "/sys/class/qcom-battery/restrict_cur" + } +} diff --git a/app/src/main/java/de/langerhans/odintools/tools/ShellExecutor.kt b/app/src/main/java/de/langerhans/odintools/tools/ShellExecutor.kt index 9ebf3bc..df8d489 100644 --- a/app/src/main/java/de/langerhans/odintools/tools/ShellExecutor.kt +++ b/app/src/main/java/de/langerhans/odintools/tools/ShellExecutor.kt @@ -3,7 +3,6 @@ package de.langerhans.odintools.tools import android.annotation.SuppressLint import android.os.IBinder import android.os.Parcel -import de.langerhans.odintools.BuildConfig import java.nio.charset.Charset import javax.inject.Inject @@ -42,88 +41,109 @@ class ShellExecutor @Inject constructor() { return Result.success(result) } - fun getIntSystemSetting(setting: String, defaultValue: Int): Int { - return executeAsRoot("settings get system $setting") + private fun getProperty(property: String): Result { + return executeAsRoot("getprop $property") + } + + fun getIntProperty(property: String, defaultValue: Int): Int { + return getProperty(property) .mapCatching { it?.toInt() ?: defaultValue } .getOrDefault(defaultValue) } - fun setIntSystemSetting(setting: String, value: Int) { - executeAsRoot("settings put system $setting $value") + fun getFloatProperty(property: String, defaultValue: Float): Float { + return getProperty(property) + .mapCatching { it?.toFloat() ?: defaultValue } + .getOrDefault(defaultValue) } - fun getBooleanSystemSetting(setting: String, defaultValue: Boolean): Boolean { - return executeAsRoot("settings get system $setting") + fun getBooleanProperty(property: String, defaultValue: Boolean): Boolean { + return getProperty(property) .map { if (it == null) defaultValue else it == "1" } .getOrDefault(defaultValue) } - fun setBooleanSystemSetting(setting: String, value: Boolean) { - setIntSystemSetting(setting, if (value) 1 else 0) + fun getStringProperty(property: String, defaultValue: String): String { + return getProperty(property) + .map { it ?: defaultValue } + .getOrDefault(defaultValue) + } + + private fun getSystemSetting(setting: String): Result { + return executeAsRoot("settings get system $setting") } fun getStringSystemSetting(setting: String, defaultValue: String): String { - return executeAsRoot("settings get system $setting").map { - it ?: defaultValue - }.getOrDefault(defaultValue) + return getSystemSetting(setting) + .map { it ?: defaultValue } + .getOrDefault(defaultValue) } fun setStringSystemSetting(setting: String, value: String) { executeAsRoot("settings put system $setting $value") } - private fun enableA11yService() { - val currentServices = - executeAsRoot("settings get secure enabled_accessibility_services") - .map { it ?: "" } - .getOrDefault("") + fun getIntSystemSetting(setting: String, defaultValue: Int): Int { + return getSystemSetting(setting) + .mapCatching { it?.toInt() ?: defaultValue } + .getOrDefault(defaultValue) + } + + fun setIntSystemSetting(setting: String, value: Int) { + executeAsRoot("settings put system $setting $value") + } - if (currentServices.contains(PACKAGE)) return + fun getBooleanSystemSetting(setting: String, defaultValue: Boolean): Boolean { + return getSystemSetting(setting) + .map { if (it == null) defaultValue else it == "1" } + .getOrDefault(defaultValue) + } - executeAsRoot( - "settings put secure enabled_accessibility_services $PACKAGE/$PACKAGE.service.ForegroundAppWatcherService:$currentServices" - .trimEnd(':'), - ) + fun setBooleanSystemSetting(setting: String, value: Boolean) { + setIntSystemSetting(setting, if (value) 1 else 0) } - fun setSfSaturation(saturation: Float) { - executeAsRoot("service call SurfaceFlinger 1022 f ${String.format("%.1f", saturation)}") + private fun getValue(file: String): Result { + return executeAsRoot("cat $file") } - fun setVibrationStrength(newValue: Int) { - executeAsRoot("echo $newValue > /d/haptics/user_vmax_mv") + fun getStringValue(file: String, defaultValue: String): String { + return getValue(file) + .map { it ?: defaultValue } + .getOrDefault(defaultValue) } - fun getVibrationStrength(): Int { - val defaultValue = 0 + fun setStringValue(file: String, value: String) { + executeAsRoot("echo $value > $file") + } - return executeAsRoot("cat /d/haptics/user_vmax_mv") - .mapCatching { it?.toInt() ?: defaultValue } // This throws on RP4 because it has no rumble + fun getIntValue(file: String, defaultValue: Int): Int { + return getValue(file) + .mapCatching { it?.toInt() ?: defaultValue } .getOrDefault(defaultValue) } - private fun grantAllAppsPermission() { - executeAsRoot("pm grant $PACKAGE android.permission.QUERY_ALL_PACKAGES") + fun setIntValue(file: String, value: Int) { + executeAsRoot("echo $value > $file") } - private fun addOdinToolsToWhitelist() { - val currentWhitelist = getStringSystemSetting("app_whiteList", "") - if (currentWhitelist.contains(PACKAGE)) return + fun getFloatValue(file: String, defaultValue: Float): Float { + return getValue(file) + .mapCatching { it?.toFloat() ?: defaultValue } + .getOrDefault(defaultValue) + } - val newWhitelist = "$PACKAGE,$currentWhitelist".trimEnd(',') - setStringSystemSetting("app_whiteList", newWhitelist) + fun setFloatValue(file: String, value: Float) { + executeAsRoot("echo $value > $file") } - fun applyRequiredSettings() { - enableA11yService() - grantAllAppsPermission() - if (!BuildConfig.DEBUG) { - // Don't add to whitelist on debug builds, otherwise even Android Studio can't kill the app - addOdinToolsToWhitelist() - } + fun getBooleanValue(file: String, defaultValue: Boolean): Boolean { + return getValue(file) + .map { if (it == null) defaultValue else it == "1" } + .getOrDefault(defaultValue) } - companion object { - private const val PACKAGE = BuildConfig.APPLICATION_ID + fun setBooleanValue(file: String, value: Boolean) { + setIntValue(file, if (value) 1 else 0) } } diff --git a/app/src/main/java/de/langerhans/odintools/ui/composables/LargeDropDownMenu.kt b/app/src/main/java/de/langerhans/odintools/ui/composables/LargeDropDownMenu.kt index e474971..5a05d63 100644 --- a/app/src/main/java/de/langerhans/odintools/ui/composables/LargeDropDownMenu.kt +++ b/app/src/main/java/de/langerhans/odintools/ui/composables/LargeDropDownMenu.kt @@ -1,7 +1,13 @@ package de.langerhans.odintools.ui.composables import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -9,8 +15,20 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/de/langerhans/odintools/ui/composables/Preferences.kt b/app/src/main/java/de/langerhans/odintools/ui/composables/Preferences.kt index 047493d..33842a9 100644 --- a/app/src/main/java/de/langerhans/odintools/ui/composables/Preferences.kt +++ b/app/src/main/java/de/langerhans/odintools/ui/composables/Preferences.kt @@ -7,11 +7,37 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RangeSlider +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -25,6 +51,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import de.langerhans.odintools.R import de.langerhans.odintools.main.CheckboxPreferenceUiModel +import de.langerhans.odintools.ui.theme.Typography +import kotlin.math.roundToInt @Composable fun SettingsHeader(@StringRes name: Int) { @@ -150,6 +178,7 @@ fun TriggerPreference(@DrawableRes icon: Int, @StringRes title: Int, @StringRes @Composable fun CheckBoxDialogPreference( items: List, + @StringRes title: Int, minSelected: Int = 2, onCancel: () -> Unit, onSave: (items: List) -> Unit, @@ -161,14 +190,13 @@ fun CheckBoxDialogPreference( }, dismissButton = { DialogButton(text = stringResource(id = R.string.cancel), onCancel) }, title = { - Text(text = stringResource(id = R.string.controllerStyle)) + Text(text = stringResource(id = title)) }, text = { LazyColumn { items(items = items, key = { item -> item.key }) { item -> fun canChangeCheckbox(): Boolean { return item.checked.not() || items.count { it.checked } == minSelected + 1 } - CheckboxDialogRow( text = stringResource(id = item.text), checked = item.checked, @@ -312,7 +340,8 @@ fun RemapButtonDialog(initialValue: Int, onCancel: () -> Unit, onReset: () -> Un ) Row( modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .padding(top = 24.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { TextButton(onClick = onReset) { @@ -335,3 +364,56 @@ fun RemapButtonDialog(initialValue: Int, onCancel: () -> Unit, onReset: () -> Un focusRequester.requestFocus() } } + +@Composable +fun ChargeLimitPreferenceDialog(initialValue: ClosedRange, onCancel: () -> Unit, onSave: (newValue: ClosedRange) -> Unit) { + var userValue by remember { + mutableStateOf(initialValue.start.toFloat()..initialValue.endInclusive.toFloat()) + } + val start = userValue.start.roundToInt() + val end = userValue.endInclusive.roundToInt() + + AlertDialog( + onDismissRequest = {}, + confirmButton = { + DialogButton(text = stringResource(id = R.string.save)) { + onSave(start..end) + } + }, + dismissButton = { + DialogButton(text = stringResource(id = R.string.cancel), onCancel) + }, + title = { + Text(text = stringResource(id = R.string.chargeLimit)) + }, + text = { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + RangeSlider( + value = userValue, + valueRange = 0f..100f, + steps = 9, + onValueChange = { + userValue = it + }, + modifier = Modifier + .weight(1f), + ) + } + Row { + Text(text = stringResource(id = R.string.chargeLimitPreferenceDialogOffAt, start)) + Spacer(modifier = Modifier.weight(1f)) + Text(text = stringResource(id = R.string.chargeLimitPreferenceDialogOnAt, end)) + } + Row { + Text( + style = Typography.labelSmall, + text = stringResource(id = R.string.chargeLimitPreferenceDialogDescription), + ) + } + } + }, + ) +} diff --git a/app/src/main/res/drawable-night/ic_add.xml b/app/src/main/res/drawable-night/ic_add.xml new file mode 100644 index 0000000..c2c3eb3 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_add.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_app_settings.xml b/app/src/main/res/drawable-night/ic_app_settings.xml index e487ecb..1a7eac7 100644 --- a/app/src/main/res/drawable-night/ic_app_settings.xml +++ b/app/src/main/res/drawable-night/ic_app_settings.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_controllerstyle.xml b/app/src/main/res/drawable-night/ic_controllerstyle.xml deleted file mode 100644 index 1bee04c..0000000 --- a/app/src/main/res/drawable-night/ic_controllerstyle.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable-night/ic_electrical_services.xml b/app/src/main/res/drawable-night/ic_electrical_services.xml new file mode 100644 index 0000000..c326ea5 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_electrical_services.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_face_buttons.xml b/app/src/main/res/drawable-night/ic_face_buttons.xml new file mode 100644 index 0000000..dcdae7c --- /dev/null +++ b/app/src/main/res/drawable-night/ic_face_buttons.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_file_save.xml b/app/src/main/res/drawable-night/ic_file_save.xml new file mode 100644 index 0000000..89ca44b --- /dev/null +++ b/app/src/main/res/drawable-night/ic_file_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-night/ic_gamepad.xml b/app/src/main/res/drawable-night/ic_gamepad.xml index dca33ef..d0a69dc 100644 --- a/app/src/main/res/drawable-night/ic_gamepad.xml +++ b/app/src/main/res/drawable-night/ic_gamepad.xml @@ -1,5 +1,9 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_home.xml b/app/src/main/res/drawable-night/ic_home.xml index c66f285..3d118d2 100644 --- a/app/src/main/res/drawable-night/ic_home.xml +++ b/app/src/main/res/drawable-night/ic_home.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_info.xml b/app/src/main/res/drawable-night/ic_info.xml index 1ece034..a9235f6 100644 --- a/app/src/main/res/drawable-night/ic_info.xml +++ b/app/src/main/res/drawable-night/ic_info.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_more_time.xml b/app/src/main/res/drawable-night/ic_more_time.xml index 7d150d0..02f065f 100644 --- a/app/src/main/res/drawable-night/ic_more_time.xml +++ b/app/src/main/res/drawable-night/ic_more_time.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_palette.xml b/app/src/main/res/drawable-night/ic_palette.xml index cb4385b..853ee77 100644 --- a/app/src/main/res/drawable-night/ic_palette.xml +++ b/app/src/main/res/drawable-night/ic_palette.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_sliders.xml b/app/src/main/res/drawable-night/ic_sliders.xml index eaa85cc..e916d26 100644 --- a/app/src/main/res/drawable-night/ic_sliders.xml +++ b/app/src/main/res/drawable-night/ic_sliders.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml index 760187a..1e43f8b 100644 --- a/app/src/main/res/drawable/ic_add.xml +++ b/app/src/main/res/drawable/ic_add.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_app_settings.xml b/app/src/main/res/drawable/ic_app_settings.xml index ad608b9..328bc54 100644 --- a/app/src/main/res/drawable/ic_app_settings.xml +++ b/app/src/main/res/drawable/ic_app_settings.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_controllerstyle.xml b/app/src/main/res/drawable/ic_controllerstyle.xml deleted file mode 100644 index 3eb98b9..0000000 --- a/app/src/main/res/drawable/ic_controllerstyle.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_electrical_services.xml b/app/src/main/res/drawable/ic_electrical_services.xml new file mode 100644 index 0000000..a6a752f --- /dev/null +++ b/app/src/main/res/drawable/ic_electrical_services.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_face_buttons.xml b/app/src/main/res/drawable/ic_face_buttons.xml new file mode 100644 index 0000000..e4abe3e --- /dev/null +++ b/app/src/main/res/drawable/ic_face_buttons.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_file_save.xml b/app/src/main/res/drawable/ic_file_save.xml new file mode 100644 index 0000000..d5383d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_gamepad.xml b/app/src/main/res/drawable/ic_gamepad.xml index f3e4143..82ce9f9 100644 --- a/app/src/main/res/drawable/ic_gamepad.xml +++ b/app/src/main/res/drawable/ic_gamepad.xml @@ -1,5 +1,8 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml index e45e971..33a4b84 100644 --- a/app/src/main/res/drawable/ic_home.xml +++ b/app/src/main/res/drawable/ic_home.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml index 2257ddf..db588db 100644 --- a/app/src/main/res/drawable/ic_info.xml +++ b/app/src/main/res/drawable/ic_info.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_more_time.xml b/app/src/main/res/drawable/ic_more_time.xml index 720de56..22fae63 100644 --- a/app/src/main/res/drawable/ic_more_time.xml +++ b/app/src/main/res/drawable/ic_more_time.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_palette.xml b/app/src/main/res/drawable/ic_palette.xml index a48e3c2..23f3974 100644 --- a/app/src/main/res/drawable/ic_palette.xml +++ b/app/src/main/res/drawable/ic_palette.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sliders.xml b/app/src/main/res/drawable/ic_sliders.xml index a8aa26b..1f21b16 100644 --- a/app/src/main/res/drawable/ic_sliders.xml +++ b/app/src/main/res/drawable/ic_sliders.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - - + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 573ff5c..b27fe68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,16 +3,16 @@ Xbox Odin Disconnect - Controller Style + Controller style Unknown - L2/R2 Mode + L2/R2 mode Analog Digital Both Performance Mode Standard Performance - High Performance + High Performance Fan Mode Off Quiet @@ -20,13 +20,13 @@ Sport Device version:\nApp version: - Quick Settings - Select which modes to cycle through in the Controller Style quick settings tile - Select which modes to cycle through in the L2/R2 Mode quick settings tile + Quick Settings + Select which modes to cycle through in the Controller Style quick settings tile. + Select which modes to cycle through in the L2/R2 Mode quick settings tile. Buttons - Single press home button - When enabled allows using the home button with a single press instead of a double press + Single-press home button + When enabled allows using the home button with a single press instead of a double press. Warning This app was made for the Ayn Odin 2. Running it on an incompatible device may cause damage. The author of this app takes no responsibility for any damage caused! @@ -42,7 +42,7 @@ Display Change the saturation of the display. Will be re-applied on reboot. Default value is 1.0. - Used by OdinTools to detect running apps for dynamic switching of button settings + Used by OdinTools to detect running apps for dynamic switching of button settings. App overrides Set up overrides per app. Switch off to globally disable app overrides. @@ -53,7 +53,7 @@ You already added overrides for all your apps Fan mode is required when setting a Performance mode Override delay - Delay applying overrides by 0.5s. This can help if apps crash when applying an + Delay applying overrides by 0.5s. This can help if apps crash when applying an override. @@ -67,4 +67,17 @@ M2 button Press any button Set Default - \ No newline at end of file + + Battery + Charge limit + When enabled activates charging separation at the specified battery levels. + Off at %1$d%% + On at %1$d%% + + Charging separation means the power goes straight to the device, bypassing the battery. + Notice that if the power source is insufficient, battery drain will occur. + + Debug + Dump log to file + Dump log output to a text file in the internal memory. + diff --git a/gradle.properties b/gradle.properties index ada445c..a2a7978 100644 --- a/gradle.properties +++ b/gradle.properties @@ -29,3 +29,4 @@ android.enableBuildConfigAsBytecode=true systemProp.javax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl systemProp.javax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl systemProp.javax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl +org.gradle.configuration-cache=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f49dee..d699884 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,17 @@ [versions] -androidx-activity = "1.8.2" +androidx-activity = "1.9.0" androidx-annotation-annotation-jvm = "1.7.1" androidx-compose-bom = "2024.02.00" -androidx-hilt = "1.1.0" +androidx-hilt = "1.2.0" androidx-lifecycle = "2.7.0" androidx-navigation = "2.7.7" androidx-room = "2.6.1" androidx-test-espresso-espresso-core = "3.5.1" androidx-test-ext-junit = "1.1.5" -com-android-application = "8.2.2" +com-android-application = "8.3.2" com-github-ben-manes-versions = "0.51.0" com-google-accompanist-accompanist-drawablepainter = "0.34.0" -com-google-dagger = "2.50" +com-google-dagger = "2.51.1" com-google-dagger-hilt-android = "2.50" com-google-devtools-ksp = "1.9.22-1.0.17" junit = "4.13.2"