Split app-level and server-level settings (#3241)

* Move server settings to a server settings screen

 - Move server-specific settings to a new screen to be accessed from the main settings only containing server specific settings in preparation for multiserver
 - Sensors is currently not server-specific even though one setting is stored by server ID, to be fixed later

* Store app preferences in another shared preferences file

 - Store app preferences not in the integration shared preferences file, but in the shared preferences file for general prefs (it is named themes but there are already other app-level prefs in it)
 - Move Wear specific preferences to it's own repository + shared preferences file

* Improve server row with local data

 - While we don't have a server name or user name, show the registration name when available to improve the server row layout

* Simplify location permission request code

 - Remove the flexible permission requests as it isn't used in the current settings structure, only expect location permissions
 - Switch from the deprecated functions to the new flow using activity result contracts

* Remove unused string
This commit is contained in:
Joris Pelgröm 2023-01-20 20:10:21 +01:00 committed by GitHub
parent ca8059da65
commit e382a4b687
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 974 additions and 760 deletions

View file

@ -12,6 +12,7 @@ import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.applyCompressedStateDiff
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
@ -83,6 +84,9 @@ class HaControlsProviderService : ControlsProviderService() {
@Inject
lateinit var urlRepository: UrlRepository
@Inject
lateinit var prefsRepository: PrefsRepository
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private var areaRegistry: List<AreaRegistryResponse>? = null
@ -364,9 +368,9 @@ class HaControlsProviderService : ControlsProviderService() {
private suspend fun entityRequiresAuth(entityId: String): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val setting = integrationRepository.getControlsAuthRequired()
val setting = prefsRepository.getControlsAuthRequired()
if (setting == ControlsAuthRequiredSetting.SELECTION) {
val includeList = integrationRepository.getControlsAuthEntities()
val includeList = prefsRepository.getControlsAuthEntities()
includeList.contains(entityId)
} else {
setting == ControlsAuthRequiredSetting.ALL

View file

@ -46,6 +46,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.notifications.NotificationData
import io.homeassistant.companion.android.common.notifications.createChannelID
@ -99,6 +100,7 @@ class MessagingManager @Inject constructor(
private val integrationUseCase: IntegrationRepository,
private val urlUseCase: UrlRepository,
private val authenticationUseCase: AuthenticationRepository,
private val prefsRepository: PrefsRepository,
private val notificationDao: NotificationDao,
private val sensorDao: SensorDao,
private val settingsDao: SettingsDao
@ -930,7 +932,7 @@ class MessagingManager @Inject constructor(
COMMAND_SCREEN_ON -> {
if (!command.isNullOrEmpty()) {
mainScope.launch {
integrationUseCase.setKeepScreenOnEnabled(
prefsRepository.setKeepScreenOnEnabled(
command == COMMAND_KEEP_SCREEN_ON
)
}

View file

@ -1,27 +1,22 @@
package io.homeassistant.companion.android.settings
import android.Manifest
import android.annotation.SuppressLint
import android.app.UiModeManager
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.text.InputType
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
@ -29,9 +24,6 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.authenticator.Authenticator
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
import io.homeassistant.companion.android.common.util.LocationPermissionInfoHandler
import io.homeassistant.companion.android.nfc.NfcSetupActivity
import io.homeassistant.companion.android.settings.controls.ManageControlsSettingsFragment
import io.homeassistant.companion.android.settings.language.LanguagesProvider
@ -41,16 +33,13 @@ import io.homeassistant.companion.android.settings.notification.NotificationHist
import io.homeassistant.companion.android.settings.qs.ManageTilesFragment
import io.homeassistant.companion.android.settings.sensor.SensorSettingsFragment
import io.homeassistant.companion.android.settings.sensor.SensorUpdateFrequencyFragment
import io.homeassistant.companion.android.settings.server.ServerSettingsFragment
import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsSettingsFragment
import io.homeassistant.companion.android.settings.ssid.SsidFragment
import io.homeassistant.companion.android.settings.url.ExternalUrlFragment
import io.homeassistant.companion.android.settings.wear.SettingsWearActivity
import io.homeassistant.companion.android.settings.wear.SettingsWearDetection
import io.homeassistant.companion.android.settings.websocket.WebsocketSettingFragment
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsSettingsFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@ -60,12 +49,10 @@ import io.homeassistant.companion.android.common.R as commonR
class SettingsFragment constructor(
val presenter: SettingsPresenter,
val langProvider: LanguagesProvider
) : PreferenceFragmentCompat(), SettingsView {
) : PreferenceFragmentCompat() {
companion object {
private const val TAG = "SettingsFragment"
private const val LOCATION_REQUEST_CODE = 0
private const val BACKGROUND_LOCATION_REQUEST_CODE = 1
}
private val requestBackgroundAccessResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@ -77,56 +64,32 @@ class SettingsFragment constructor(
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
presenter.init(this)
preferenceManager.preferenceDataStore = presenter.getPreferenceDataStore()
setPreferencesFromResource(R.xml.preferences, rootKey)
val onChangeUrlValidator = Preference.OnPreferenceChangeListener { _, newValue ->
val isValid = newValue.toString().isBlank() || newValue.toString().toHttpUrlOrNull() != null
if (!isValid) {
AlertDialog.Builder(requireActivity())
.setTitle(commonR.string.url_invalid)
.setMessage(commonR.string.url_parse_error)
.setPositiveButton(android.R.string.ok) { _, _ -> }
.show()
// This should enumerate over all servers in the future
val serverPreference = Preference(requireContext())
presenter.getServerRegistrationName()?.let {
serverPreference.title = it
serverPreference.summary = presenter.getServerName()
} ?: run {
serverPreference.title = presenter.getServerName()
}
serverPreference.order = 1
try {
serverPreference.icon = AppCompatResources.getDrawable(requireContext(), commonR.drawable.ic_stat_ic_notification_blue)
} catch (e: Exception) {
Log.e(TAG, "Unable to set the server icon", e)
}
serverPreference.setOnPreferenceClickListener {
parentFragmentManager.commit {
replace(R.id.content, ServerSettingsFragment::class.java, null)
addToBackStack(getString(commonR.string.server_settings))
}
isValid
}
findPreference<SwitchPreference>("app_lock")?.setOnPreferenceChangeListener { _, newValue ->
val isValid: Boolean
if (newValue == false) {
isValid = true
findPreference<SwitchPreference>("app_lock_home_bypass")?.isVisible = false
findPreference<EditTextPreference>("session_timeout")?.isVisible = false
} else {
val settingsActivity = requireActivity() as SettingsActivity
val canAuth = settingsActivity.requestAuthentication(getString(commonR.string.biometric_set_title), ::setLockAuthenticationResult)
isValid = canAuth
if (!canAuth) {
AlertDialog.Builder(requireActivity())
.setTitle(commonR.string.set_lock_title)
.setMessage(commonR.string.set_lock_message)
.setPositiveButton(android.R.string.ok) { _, _ -> }
.show()
}
}
isValid
}
findPreference<SwitchPreference>("app_lock_home_bypass")?.let {
it.isVisible = findPreference<SwitchPreference>("app_lock")?.isChecked == true
}
findPreference<EditTextPreference>("session_timeout")?.let { pref ->
pref.setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_NUMBER
}
pref.isVisible = findPreference<SwitchPreference>("app_lock")?.isChecked == true
return@setOnPreferenceClickListener true
}
findPreference<PreferenceCategory>("servers_devices_category")?.addPreference(serverPreference)
findPreference<Preference>("nfc_tags")?.let {
val pm: PackageManager = requireContext().packageManager
@ -141,54 +104,29 @@ class SettingsFragment constructor(
updateBackgroundAccessPref()
findPreference<EditTextPreference>("connection_internal")?.let {
it.onPreferenceChangeListener =
onChangeUrlValidator
}
findPreference<Preference>("connection_external")?.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, ExternalUrlFragment::class.java, null)
.addToBackStack(getString(commonR.string.input_url))
.commit()
return@setOnPreferenceClickListener true
}
findPreference<Preference>("connection_internal_ssids")?.let {
it.setOnPreferenceClickListener {
onDisplaySsidScreen()
return@setOnPreferenceClickListener true
}
}
findPreference<Preference>("sensors")?.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, SensorSettingsFragment::class.java, null)
.addToBackStack(getString(commonR.string.sensors))
.commit()
parentFragmentManager.commit {
replace(R.id.content, SensorSettingsFragment::class.java, null)
addToBackStack(getString(commonR.string.sensors))
}
return@setOnPreferenceClickListener true
}
findPreference<Preference>("sensor_update_frequency")?.let {
it.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, SensorUpdateFrequencyFragment::class.java, null)
.addToBackStack(getString(commonR.string.sensor_update_frequency))
.commit()
parentFragmentManager.commit {
replace(R.id.content, SensorUpdateFrequencyFragment::class.java, null)
addToBackStack(getString(commonR.string.sensor_update_frequency))
}
return@setOnPreferenceClickListener true
}
}
findPreference<PreferenceCategory>("widgets")?.isVisible = Build.MODEL != "Quest"
findPreference<PreferenceCategory>("security_category")?.isVisible = Build.MODEL != "Quest"
findPreference<Preference>("manage_widgets")?.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, ManageWidgetsSettingsFragment::class.java, null)
.addToBackStack(getString(commonR.string.widgets))
.commit()
parentFragmentManager.commit {
replace(R.id.content, ManageWidgetsSettingsFragment::class.java, null)
addToBackStack(getString(commonR.string.widgets))
}
return@setOnPreferenceClickListener true
}
@ -198,11 +136,10 @@ class SettingsFragment constructor(
it.isVisible = true
}
findPreference<Preference>("manage_shortcuts")?.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, ManageShortcutsSettingsFragment::class.java, null)
.addToBackStack(getString(commonR.string.shortcuts))
.commit()
parentFragmentManager.commit {
replace(R.id.content, ManageShortcutsSettingsFragment::class.java, null)
addToBackStack(getString(commonR.string.shortcuts))
}
return@setOnPreferenceClickListener true
}
}
@ -212,11 +149,10 @@ class SettingsFragment constructor(
it.isVisible = true
}
findPreference<Preference>("manage_tiles")?.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, ManageTilesFragment::class.java, null)
.addToBackStack(getString(commonR.string.tiles))
.commit()
parentFragmentManager.commit {
replace(R.id.content, ManageTilesFragment::class.java, null)
addToBackStack(getString(commonR.string.tiles))
}
return@setOnPreferenceClickListener true
}
}
@ -226,11 +162,10 @@ class SettingsFragment constructor(
it.isVisible = true
}
findPreference<Preference>("manage_device_controls")?.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, ManageControlsSettingsFragment::class.java, null)
.addToBackStack(getString(commonR.string.controls_setting_title))
.commit()
parentFragmentManager.commit {
replace(R.id.content, ManageControlsSettingsFragment::class.java, null)
addToBackStack(getString(commonR.string.controls_setting_title))
}
return@setOnPreferenceClickListener true
}
}
@ -240,17 +175,6 @@ class SettingsFragment constructor(
it.isVisible = true
}
findPreference<Preference>("websocket")?.let {
it.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, WebsocketSettingFragment::class.java, null)
.addToBackStack(getString(commonR.string.notifications))
.commit()
return@setOnPreferenceClickListener true
}
}
updateNotificationChannelPrefs()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -263,11 +187,10 @@ class SettingsFragment constructor(
findPreference<Preference>("notification_channels")?.let { pref ->
pref.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, NotificationChannelFragment::class.java, null)
.addToBackStack(getString(commonR.string.notification_channels))
.commit()
parentFragmentManager.commit {
replace(R.id.content, NotificationChannelFragment::class.java, null)
addToBackStack(getString(commonR.string.notification_channels))
}
return@setOnPreferenceClickListener true
}
}
@ -276,11 +199,10 @@ class SettingsFragment constructor(
findPreference<Preference>("notification_history")?.let {
it.isVisible = true
it.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, NotificationHistoryFragment::class.java, null)
.addToBackStack(getString(commonR.string.notifications))
.commit()
parentFragmentManager.commit {
replace(R.id.content, NotificationHistoryFragment::class.java, null)
addToBackStack(getString(commonR.string.notifications))
}
return@setOnPreferenceClickListener true
}
}
@ -319,11 +241,12 @@ class SettingsFragment constructor(
}
lifecycleScope.launch {
findPreference<PreferenceCategory>("wear_category")?.isVisible =
SettingsWearDetection.hasAnyNodes(requireContext())
findPreference<Preference>("wear_settings")?.setOnPreferenceClickListener {
startActivity(SettingsWearActivity.newInstance(requireContext()))
return@setOnPreferenceClickListener true
findPreference<Preference>("wear_settings")?.let {
it.isVisible = SettingsWearDetection.hasAnyNodes(requireContext())
it.setOnPreferenceClickListener {
startActivity(SettingsWearActivity.newInstance(requireContext()))
return@setOnPreferenceClickListener true
}
}
}
@ -357,142 +280,12 @@ class SettingsFragment constructor(
}
findPreference<Preference>("show_share_logs")?.setOnPreferenceClickListener {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, LogFragment::class.java, null)
.addToBackStack(getString(commonR.string.log))
.commit()
parentFragmentManager.commit {
replace(R.id.content, LogFragment::class.java, null)
addToBackStack(getString(commonR.string.log))
}
return@setOnPreferenceClickListener true
}
presenter.onCreate()
}
override fun disableInternalConnection() {
findPreference<EditTextPreference>("connection_internal")?.let {
it.isEnabled = false
try {
val unwrappedDrawable =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_computer)
unwrappedDrawable?.setTint(Color.DKGRAY)
it.icon = unwrappedDrawable
} catch (e: Exception) {
Log.e(TAG, "Unable to set the icon tint", e)
}
}
findPreference<SwitchPreference>("app_lock_home_bypass")?.let {
it.isEnabled = false
try {
val unwrappedDrawable =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_wifi)
unwrappedDrawable?.setTint(Color.DKGRAY)
it.icon = unwrappedDrawable
} catch (e: Exception) {
Log.e(TAG, "Unable to set the icon tint", e)
}
}
}
override fun enableInternalConnection() {
findPreference<EditTextPreference>("connection_internal")?.let {
it.isEnabled = true
try {
val unwrappedDrawable =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_computer)
unwrappedDrawable?.setTint(resources.getColor(commonR.color.colorAccent))
it.icon = unwrappedDrawable
} catch (e: Exception) {
Log.e(TAG, "Unable to set the icon tint", e)
}
}
findPreference<SwitchPreference>("app_lock_home_bypass")?.let {
it.isEnabled = true
try {
val unwrappedDrawable =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_wifi)
unwrappedDrawable?.setTint(resources.getColor(commonR.color.colorAccent))
it.icon = unwrappedDrawable
} catch (e: Exception) {
Log.e(TAG, "Unable to set the icon tint", e)
}
}
}
override fun updateExternalUrl(url: String, useCloud: Boolean) {
findPreference<Preference>("connection_external")?.let {
it.summary =
if (useCloud) getString(commonR.string.input_cloud)
else url
}
}
override fun updateSsids(ssids: Set<String>) {
findPreference<Preference>("connection_internal_ssids")?.let {
it.summary =
if (ssids.isEmpty()) getString(commonR.string.pref_connection_ssids_empty)
else ssids.joinToString()
}
}
private fun onDisplaySsidScreen() {
val permissionsToCheck: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
} else {
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (DisabledLocationHandler.isLocationEnabled(requireContext())) {
var permissionsToRequest: Array<String>? = null
if (permissionsToCheck.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// For Android 11 we MUST NOT request Background Location permission with fine or coarse permissions
// as for Android 11 the background location request needs to be done separately
// See here: https://developer.android.com/about/versions/11/privacy/location#request-background-location-separately
permissionsToRequest = permissionsToCheck.toList().minus(Manifest.permission.ACCESS_BACKGROUND_LOCATION).toTypedArray()
}
val hasPermission = checkPermission(permissionsToCheck)
if (permissionsToCheck.isNotEmpty() && !hasPermission) {
LocationPermissionInfoHandler.showLocationPermInfoDialogIfNeeded(
requireContext(), permissionsToCheck,
continueYesCallback = {
checkAndRequestPermissions(permissionsToCheck, LOCATION_REQUEST_CODE, permissionsToRequest, true)
// showSsidSettings() will be called in onRequestPermissionsResult if permission is granted
}
)
} else showSsidSettings()
} else {
if (presenter.isSsidUsed()) {
DisabledLocationHandler.showLocationDisabledWarnDialog(requireActivity(), arrayOf(getString(commonR.string.pref_connection_wifi)), showAsNotification = false, withDisableOption = true) {
presenter.clearSsids()
presenter.updateInternalUrlStatus()
}
} else {
DisabledLocationHandler.showLocationDisabledWarnDialog(requireActivity(), arrayOf(getString(commonR.string.pref_connection_wifi)))
}
}
}
private fun showSsidSettings() {
parentFragmentManager
.beginTransaction()
.replace(R.id.content, SsidFragment::class.java, null)
.addToBackStack(getString(commonR.string.manage_ssids))
.commit()
}
private fun setLockAuthenticationResult(result: Int): Boolean {
val success = result == Authenticator.SUCCESS
val switchLock = findPreference<SwitchPreference>("app_lock")
switchLock?.isChecked = success
// Prevent requesting authentication after just enabling the app lock
presenter.setAppActive(success)
findPreference<SwitchPreference>("app_lock_home_bypass")?.isVisible = success
findPreference<EditTextPreference>("session_timeout")?.isVisible = success
return (result == Authenticator.SUCCESS || result == Authenticator.CANCELED)
}
private fun removeSystemFromThemesIfNeeded() {
@ -572,32 +365,6 @@ class SettingsFragment constructor(
}
}
private fun checkAndRequestPermissions(permissions: Array<String>, requestCode: Int, requestPermissions: Array<String>? = null, forceRequest: Boolean = false): Boolean {
val permissionsNeeded = mutableListOf<String>()
for (permission in permissions) {
if (forceRequest || ContextCompat.checkSelfPermission(requireContext(), permission) === PackageManager.PERMISSION_DENIED) {
if (requestPermissions.isNullOrEmpty() || requestPermissions.contains(permission)) {
permissionsNeeded.add(permission)
}
}
}
return if (permissionsNeeded.isNotEmpty()) {
requestPermissions(permissionsNeeded.toTypedArray(), requestCode)
false
} else true
}
fun checkPermission(permissions: Array<String>?): Boolean {
if (!permissions.isNullOrEmpty()) {
for (permission in permissions) {
if (ContextCompat.checkSelfPermission(requireContext(), permission) === PackageManager.PERMISSION_DENIED) {
return false
}
}
}
return true
}
private fun isIgnoringBatteryOptimizations(): Boolean {
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.M ||
context?.getSystemService<PowerManager>()
@ -605,38 +372,13 @@ class SettingsFragment constructor(
?: false
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val isGreaterR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
if (requestCode == LOCATION_REQUEST_CODE && grantResults.isNotEmpty()) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
if (isGreaterR) {
// For Android 11 we MUST NOT request Background Location permission with fine or coarse permissions
// as for Android 11 the background location request needs to be done separately
// See here: https://developer.android.com/about/versions/11/privacy/location#request-background-location-separately
// The separate request of background location is done here
requestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), BACKGROUND_LOCATION_REQUEST_CODE)
}
}
}
if ((requestCode == LOCATION_REQUEST_CODE && !isGreaterR || requestCode == BACKGROUND_LOCATION_REQUEST_CODE && isGreaterR) && grantResults.isNotEmpty()) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
showSsidSettings()
}
}
}
override fun onResume() {
super.onResume()
activity?.title = getString(commonR.string.companion_app)
}
presenter.updateExternalUrlStatus()
presenter.updateInternalUrlStatus()
override fun onDestroy() {
presenter.onFinish()
super.onDestroy()
}
}

View file

@ -1,21 +1,18 @@
package io.homeassistant.companion.android.settings
import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import io.homeassistant.companion.android.settings.server.ServerSettingsPresenter
import io.homeassistant.companion.android.settings.server.ServerSettingsPresenterImpl
@Module
@InstallIn(ActivityComponent::class)
abstract class SettingsModule {
companion object {
@Provides
fun settingsView(@ActivityContext context: Context): SettingsView = context as SettingsView
}
@Binds
abstract fun serverSettingsPresenter(serverSettingsPresenterImpl: ServerSettingsPresenterImpl): ServerSettingsPresenter
@Binds
abstract fun settingsPresenter(settingsPresenterImpl: SettingsPresenterImpl): SettingsPresenter

View file

@ -5,16 +5,10 @@ import androidx.preference.PreferenceDataStore
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse
interface SettingsPresenter {
fun init(settingsView: SettingsView)
fun getPreferenceDataStore(): PreferenceDataStore
fun onCreate()
fun onFinish()
fun updateExternalUrlStatus()
fun updateInternalUrlStatus()
fun setAppActive(active: Boolean)
fun getServerRegistrationName(): String?
fun getServerName(): String
suspend fun getNotificationRateLimits(): RateLimitResponse?
fun isSsidUsed(): Boolean
fun clearSsids()
fun showChangeLog(context: Context)
}

View file

@ -3,8 +3,6 @@ package io.homeassistant.companion.android.settings
import android.content.Context
import android.util.Log
import androidx.preference.PreferenceDataStore
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
@ -24,7 +22,6 @@ import javax.inject.Inject
class SettingsPresenterImpl @Inject constructor(
private val urlUseCase: UrlRepository,
private val integrationUseCase: IntegrationRepository,
private val authenticationUseCase: AuthenticationRepository,
private val prefsRepository: PrefsRepository,
private val themesManager: ThemesManager,
private val langsManager: LanguagesManager,
@ -36,77 +33,46 @@ class SettingsPresenterImpl @Inject constructor(
}
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
private lateinit var settingsView: SettingsView
override fun init(settingsView: SettingsView) {
this.settingsView = settingsView
}
override fun getBoolean(key: String, defValue: Boolean): Boolean {
return runBlocking {
return@runBlocking when (key) {
"fullscreen" -> integrationUseCase.isFullScreenEnabled()
"keep_screen_on" -> integrationUseCase.isKeepScreenOnEnabled()
"pinch_to_zoom" -> integrationUseCase.isPinchToZoomEnabled()
"app_lock" -> authenticationUseCase.isLockEnabledRaw()
"app_lock_home_bypass" -> authenticationUseCase.isLockHomeBypassEnabled()
"crash_reporting" -> prefsRepository.isCrashReporting()
"autoplay_video" -> integrationUseCase.isAutoPlayVideoEnabled()
"always_show_first_view_on_app_start" -> integrationUseCase.isAlwaysShowFirstViewOnAppStartEnabled()
"webview_debug" -> integrationUseCase.isWebViewDebugEnabled()
else -> throw IllegalArgumentException("No boolean found by this key: $key")
}
override fun getBoolean(key: String, defValue: Boolean): Boolean = runBlocking {
return@runBlocking when (key) {
"fullscreen" -> prefsRepository.isFullScreenEnabled()
"keep_screen_on" -> prefsRepository.isKeepScreenOnEnabled()
"pinch_to_zoom" -> prefsRepository.isPinchToZoomEnabled()
"crash_reporting" -> prefsRepository.isCrashReporting()
"autoplay_video" -> prefsRepository.isAutoPlayVideoEnabled()
"always_show_first_view_on_app_start" -> prefsRepository.isAlwaysShowFirstViewOnAppStartEnabled()
"webview_debug" -> prefsRepository.isWebViewDebugEnabled()
else -> throw IllegalArgumentException("No boolean found by this key: $key")
}
}
override fun putBoolean(key: String, value: Boolean) {
mainScope.launch {
when (key) {
"fullscreen" -> integrationUseCase.setFullScreenEnabled(value)
"keep_screen_on" -> integrationUseCase.setKeepScreenOnEnabled(value)
"pinch_to_zoom" -> integrationUseCase.setPinchToZoomEnabled(value)
"app_lock" -> authenticationUseCase.setLockEnabled(value)
"app_lock_home_bypass" -> authenticationUseCase.setLockHomeBypassEnabled(value)
"fullscreen" -> prefsRepository.setFullScreenEnabled(value)
"keep_screen_on" -> prefsRepository.setKeepScreenOnEnabled(value)
"pinch_to_zoom" -> prefsRepository.setPinchToZoomEnabled(value)
"crash_reporting" -> prefsRepository.setCrashReporting(value)
"autoplay_video" -> integrationUseCase.setAutoPlayVideo(value)
"always_show_first_view_on_app_start" -> integrationUseCase.setAlwaysShowFirstViewOnAppStart(value)
"webview_debug" -> integrationUseCase.setWebViewDebugEnabled(value)
"autoplay_video" -> prefsRepository.setAutoPlayVideo(value)
"always_show_first_view_on_app_start" -> prefsRepository.setAlwaysShowFirstViewOnAppStart(value)
"webview_debug" -> prefsRepository.setWebViewDebugEnabled(value)
else -> throw IllegalArgumentException("No boolean found by this key: $key")
}
}
}
override fun getString(key: String, defValue: String?): String? {
return runBlocking {
when (key) {
"connection_internal" -> (urlUseCase.getUrl(isInternal = true, force = true) ?: "").toString()
"registration_name" -> integrationUseCase.getRegistration().deviceName
"session_timeout" -> integrationUseCase.getSessionTimeOut().toString()
"themes" -> themesManager.getCurrentTheme()
"languages" -> langsManager.getCurrentLang()
else -> throw IllegalArgumentException("No string found by this key: $key")
}
override fun getString(key: String, defValue: String?): String? = runBlocking {
when (key) {
"themes" -> themesManager.getCurrentTheme()
"languages" -> langsManager.getCurrentLang()
else -> throw IllegalArgumentException("No string found by this key: $key")
}
}
override fun putString(key: String, value: String?) {
mainScope.launch {
when (key) {
"connection_internal" -> urlUseCase.saveUrl(value ?: "", true)
"session_timeout" -> {
try {
integrationUseCase.sessionTimeOut(value.toString().toInt())
} catch (e: Exception) {
Log.e(TAG, "Issue saving session timeout value", e)
}
}
"registration_name" -> {
try {
integrationUseCase.updateRegistration(DeviceRegistration(deviceName = value!!))
} catch (e: Exception) {
Log.e(TAG, "Issue updating registration with new device name", e)
}
}
"themes" -> themesManager.saveTheme(value)
"languages" -> langsManager.saveLang(value)
else -> throw IllegalArgumentException("No string found by this key: $key")
@ -114,68 +80,20 @@ class SettingsPresenterImpl @Inject constructor(
}
}
override fun getInt(key: String, defValue: Int): Int {
return runBlocking {
when (key) {
"session_timeout" -> integrationUseCase.getSessionTimeOut()
else -> throw IllegalArgumentException("No int found by this key: $key")
}
}
}
override fun putInt(key: String, value: Int) {
mainScope.launch {
when (key) {
"session_timeout" -> integrationUseCase.sessionTimeOut(value)
else -> throw IllegalArgumentException("No int found by this key: $key")
}
}
}
override fun getPreferenceDataStore(): PreferenceDataStore {
return this
}
override fun onCreate() {
mainScope.launch {
handleInternalUrlStatus(urlUseCase.getHomeWifiSsids())
updateExternalUrlStatus()
}
}
override fun onFinish() {
mainScope.cancel()
}
override fun updateExternalUrlStatus() {
mainScope.launch {
settingsView.updateExternalUrl(
urlUseCase.getUrl(false)?.toString() ?: "",
urlUseCase.shouldUseCloud() && urlUseCase.canUseCloud()
)
}
override fun getServerRegistrationName(): String? = runBlocking {
integrationUseCase.getRegistration().deviceName
}
override fun updateInternalUrlStatus() {
mainScope.launch {
handleInternalUrlStatus(urlUseCase.getHomeWifiSsids())
}
}
private suspend fun handleInternalUrlStatus(ssids: Set<String>) {
if (ssids.isEmpty()) {
settingsView.disableInternalConnection()
urlUseCase.saveUrl("", true)
} else {
settingsView.enableInternalConnection()
}
settingsView.updateSsids(ssids)
}
override fun setAppActive(active: Boolean) {
runBlocking {
integrationUseCase.setAppActive(active)
}
override fun getServerName(): String = runBlocking {
urlUseCase.getUrl()?.toString() ?: ""
}
override suspend fun getNotificationRateLimits(): RateLimitResponse? = withContext(Dispatchers.IO) {
@ -187,18 +105,6 @@ class SettingsPresenterImpl @Inject constructor(
}
}
override fun clearSsids() {
mainScope.launch {
urlUseCase.saveHomeWifiSsids(emptySet())
}
}
override fun isSsidUsed(): Boolean {
return runBlocking {
urlUseCase.getHomeWifiSsids().isNotEmpty()
}
}
override fun showChangeLog(context: Context) {
changeLog.showChangeLog(context, true)
}

View file

@ -1,12 +0,0 @@
package io.homeassistant.companion.android.settings
interface SettingsView {
fun disableInternalConnection()
fun enableInternalConnection()
fun updateExternalUrl(url: String, useCloud: Boolean)
fun updateSsids(ssids: Set<String>)
}

View file

@ -14,6 +14,7 @@ import io.homeassistant.companion.android.common.data.integration.ControlsAuthRe
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.controls.HaControlsProviderService
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -22,6 +23,7 @@ import javax.inject.Inject
@HiltViewModel
class ManageControlsViewModel @Inject constructor(
private val integrationUseCase: IntegrationRepository,
private val prefsRepository: PrefsRepository,
application: Application
) : AndroidViewModel(application) {
@ -37,8 +39,8 @@ class ManageControlsViewModel @Inject constructor(
init {
viewModelScope.launch {
authRequired = integrationUseCase.getControlsAuthRequired()
authRequiredList.addAll(integrationUseCase.getControlsAuthEntities())
authRequired = prefsRepository.getControlsAuthRequired()
authRequiredList.addAll(prefsRepository.getControlsAuthEntities())
val entities = integrationUseCase.getEntities()
?.filter { it.domain in HaControlsProviderService.getSupportedDomains() }
@ -59,8 +61,8 @@ class ManageControlsViewModel @Inject constructor(
authRequired = setting
if (authRequired != ControlsAuthRequiredSetting.SELECTION) authRequiredList.clear()
integrationUseCase.setControlsAuthRequired(setting)
integrationUseCase.setControlsAuthEntities(authRequiredList.toList())
prefsRepository.setControlsAuthRequired(setting)
prefsRepository.setControlsAuthEntities(authRequiredList.toList())
}
}
@ -89,8 +91,8 @@ class ManageControlsViewModel @Inject constructor(
// Set values for update
authRequired = newAuthRequired
if (newAuthRequired != ControlsAuthRequiredSetting.SELECTION) authRequiredList.clear()
integrationUseCase.setControlsAuthRequired(newAuthRequired)
integrationUseCase.setControlsAuthEntities(authRequiredList.toList())
prefsRepository.setControlsAuthRequired(newAuthRequired)
prefsRepository.setControlsAuthEntities(authRequiredList.toList())
}
}
}

View file

@ -0,0 +1,300 @@
package io.homeassistant.companion.android.settings.server
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.fragment.app.commit
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.authenticator.Authenticator
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
import io.homeassistant.companion.android.common.util.LocationPermissionInfoHandler
import io.homeassistant.companion.android.settings.SettingsActivity
import io.homeassistant.companion.android.settings.ssid.SsidFragment
import io.homeassistant.companion.android.settings.url.ExternalUrlFragment
import io.homeassistant.companion.android.settings.websocket.WebsocketSettingFragment
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() {
companion object {
private const val TAG = "ServerSettingsFragment"
}
@Inject
lateinit var presenter: ServerSettingsPresenter
private val permissionsRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
onPermissionsResult(it)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
presenter.init(this)
preferenceManager.preferenceDataStore = presenter.getPreferenceDataStore()
setPreferencesFromResource(R.xml.preferences_server, rootKey)
val onChangeUrlValidator = Preference.OnPreferenceChangeListener { _, newValue ->
val isValid = newValue.toString().isBlank() || newValue.toString().toHttpUrlOrNull() != null
if (!isValid) {
AlertDialog.Builder(requireActivity())
.setTitle(io.homeassistant.companion.android.common.R.string.url_invalid)
.setMessage(io.homeassistant.companion.android.common.R.string.url_parse_error)
.setPositiveButton(android.R.string.ok) { _, _ -> }
.show()
}
isValid
}
findPreference<SwitchPreference>("app_lock")?.setOnPreferenceChangeListener { _, newValue ->
val isValid: Boolean
if (newValue == false) {
isValid = true
findPreference<SwitchPreference>("app_lock_home_bypass")?.isVisible = false
findPreference<EditTextPreference>("session_timeout")?.isVisible = false
} else {
val settingsActivity = requireActivity() as SettingsActivity
val canAuth = settingsActivity.requestAuthentication(getString(io.homeassistant.companion.android.common.R.string.biometric_set_title), ::setLockAuthenticationResult)
isValid = canAuth
if (!canAuth) {
AlertDialog.Builder(requireActivity())
.setTitle(io.homeassistant.companion.android.common.R.string.set_lock_title)
.setMessage(io.homeassistant.companion.android.common.R.string.set_lock_message)
.setPositiveButton(android.R.string.ok) { _, _ -> }
.show()
}
}
isValid
}
findPreference<SwitchPreference>("app_lock_home_bypass")?.let {
it.isVisible = findPreference<SwitchPreference>("app_lock")?.isChecked == true
}
findPreference<EditTextPreference>("session_timeout")?.let { pref ->
pref.setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_NUMBER
}
pref.isVisible = findPreference<SwitchPreference>("app_lock")?.isChecked == true
}
findPreference<EditTextPreference>("connection_internal")?.let {
it.onPreferenceChangeListener =
onChangeUrlValidator
}
findPreference<Preference>("connection_external")?.setOnPreferenceClickListener {
parentFragmentManager.commit {
replace(R.id.content, ExternalUrlFragment::class.java, null)
addToBackStack(getString(io.homeassistant.companion.android.common.R.string.input_url))
}
return@setOnPreferenceClickListener true
}
findPreference<Preference>("connection_internal_ssids")?.let {
it.setOnPreferenceClickListener {
onDisplaySsidScreen()
return@setOnPreferenceClickListener true
}
}
findPreference<PreferenceCategory>("security_category")?.isVisible = Build.MODEL != "Quest"
findPreference<Preference>("websocket")?.let {
it.setOnPreferenceClickListener {
parentFragmentManager.commit {
replace(R.id.content, WebsocketSettingFragment::class.java, null)
addToBackStack(getString(io.homeassistant.companion.android.common.R.string.notifications))
}
return@setOnPreferenceClickListener true
}
}
}
override fun enableInternalConnection(isEnabled: Boolean) {
val iconTint = if (isEnabled) ContextCompat.getColor(requireContext(), commonR.color.colorAccent) else Color.DKGRAY
val doEnable = isEnabled && hasLocationPermission()
findPreference<EditTextPreference>("connection_internal")?.let {
it.isEnabled = doEnable
try {
val unwrappedDrawable =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_computer)
unwrappedDrawable?.setTint(iconTint)
it.icon = unwrappedDrawable
} catch (e: Exception) {
Log.e(TAG, "Unable to set the icon tint", e)
}
}
findPreference<SwitchPreference>("app_lock_home_bypass")?.let {
it.isEnabled = doEnable
try {
val unwrappedDrawable =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_wifi)
unwrappedDrawable?.setTint(iconTint)
it.icon = unwrappedDrawable
} catch (e: Exception) {
Log.e(TAG, "Unable to set the icon tint", e)
}
}
}
override fun updateExternalUrl(url: String, useCloud: Boolean) {
findPreference<Preference>("connection_external")?.let {
it.summary =
if (useCloud) getString(io.homeassistant.companion.android.common.R.string.input_cloud)
else url
}
}
override fun updateSsids(ssids: Set<String>) {
findPreference<Preference>("connection_internal_ssids")?.let {
it.summary =
if (ssids.isEmpty()) getString(io.homeassistant.companion.android.common.R.string.pref_connection_ssids_empty)
else ssids.joinToString()
}
}
private fun onDisplaySsidScreen() {
val permissionsToCheck: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
} else {
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (DisabledLocationHandler.isLocationEnabled(requireContext())) {
if (!checkPermission(permissionsToCheck)) {
LocationPermissionInfoHandler.showLocationPermInfoDialogIfNeeded(
requireContext(), permissionsToCheck,
continueYesCallback = {
requestLocationPermission()
// showSsidSettings() will be called if permission is granted
}
)
} else showSsidSettings()
} else {
if (presenter.isSsidUsed()) {
DisabledLocationHandler.showLocationDisabledWarnDialog(
requireActivity(),
arrayOf(
getString(
io.homeassistant.companion.android.common.R.string.pref_connection_wifi
)
),
showAsNotification = false, withDisableOption = true
) {
presenter.clearSsids()
}
} else {
DisabledLocationHandler.showLocationDisabledWarnDialog(
requireActivity(),
arrayOf(
getString(
io.homeassistant.companion.android.common.R.string.pref_connection_wifi
)
)
)
}
}
}
private fun showSsidSettings() {
parentFragmentManager.commit {
replace(R.id.content, SsidFragment::class.java, null)
addToBackStack(getString(io.homeassistant.companion.android.common.R.string.manage_ssids))
}
}
private fun setLockAuthenticationResult(result: Int): Boolean {
val success = result == Authenticator.SUCCESS
val switchLock = findPreference<SwitchPreference>("app_lock")
switchLock?.isChecked = success
// Prevent requesting authentication after just enabling the app lock
presenter.setAppActive()
findPreference<SwitchPreference>("app_lock_home_bypass")?.isVisible = success
findPreference<EditTextPreference>("session_timeout")?.isVisible = success
return (result == Authenticator.SUCCESS || result == Authenticator.CANCELED)
}
private fun hasLocationPermission(): Boolean {
val permissionsToCheck: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
} else {
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
}
return checkPermission(permissionsToCheck)
}
private fun requestLocationPermission() {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) // Background location will be requested later
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
} else {
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
}
permissionsRequest.launch(permissions)
}
private fun checkPermission(permissions: Array<String>?): Boolean {
if (!permissions.isNullOrEmpty()) {
for (permission in permissions) {
if (ContextCompat.checkSelfPermission(requireContext(), permission) == PackageManager.PERMISSION_DENIED) {
return false
}
}
}
return true
}
private fun onPermissionsResult(results: Map<String, Boolean>) {
if (results.keys.contains(Manifest.permission.ACCESS_FINE_LOCATION) &&
results[Manifest.permission.ACCESS_FINE_LOCATION] == true &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
) {
// For Android 11+ we MUST NOT request Background Location permission with fine or coarse
// permissions as for Android 11 the background location request needs to be done separately
// See here: https://developer.android.com/about/versions/11/privacy/location#request-background-location-separately
// The separate request of background location is done here
permissionsRequest.launch(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION))
return
}
if (results.entries.all { it.value }) {
showSsidSettings()
}
}
override fun onResume() {
super.onResume()
activity?.title = getString(commonR.string.server_settings)
presenter.updateUrlStatus()
}
override fun onDestroy() {
presenter.onFinish()
super.onDestroy()
}
}

View file

@ -0,0 +1,15 @@
package io.homeassistant.companion.android.settings.server
import androidx.preference.PreferenceDataStore
interface ServerSettingsPresenter {
fun init(view: ServerSettingsView)
fun getPreferenceDataStore(): PreferenceDataStore
fun onFinish()
fun updateUrlStatus()
fun isSsidUsed(): Boolean
fun clearSsids()
fun setAppActive()
}

View file

@ -0,0 +1,120 @@
package io.homeassistant.companion.android.settings.server
import android.util.Log
import androidx.preference.PreferenceDataStore
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
class ServerSettingsPresenterImpl @Inject constructor(
private val authenticationRepository: AuthenticationRepository,
private val integrationRepository: IntegrationRepository,
private val urlRepository: UrlRepository
) : ServerSettingsPresenter, PreferenceDataStore() {
companion object {
private const val TAG = "ServerSettingsPresImpl"
}
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
private lateinit var view: ServerSettingsView
override fun init(view: ServerSettingsView) {
this.view = view
}
override fun getPreferenceDataStore(): PreferenceDataStore = this
override fun getBoolean(key: String?, defValue: Boolean): Boolean = runBlocking {
when (key) {
"app_lock" -> authenticationRepository.isLockEnabledRaw()
"app_lock_home_bypass" -> authenticationRepository.isLockHomeBypassEnabled()
else -> throw IllegalArgumentException("No boolean found by this key: $key")
}
}
override fun putBoolean(key: String?, value: Boolean) {
mainScope.launch {
when (key) {
"app_lock" -> authenticationRepository.setLockEnabled(value)
"app_lock_home_bypass" -> authenticationRepository.setLockHomeBypassEnabled(value)
else -> throw IllegalArgumentException("No boolean found by this key: $key")
}
}
}
override fun getString(key: String?, defValue: String?): String? = runBlocking {
when (key) {
"connection_internal" -> (urlRepository.getUrl(isInternal = true, force = true) ?: "").toString()
"registration_name" -> integrationRepository.getRegistration().deviceName
"session_timeout" -> integrationRepository.getSessionTimeOut().toString()
else -> throw IllegalArgumentException("No string found by this key: $key")
}
}
override fun putString(key: String?, value: String?) {
mainScope.launch {
when (key) {
"connection_internal" -> urlRepository.saveUrl(value ?: "", true)
"session_timeout" -> {
try {
integrationRepository.sessionTimeOut(value.toString().toInt())
} catch (e: Exception) {
Log.e(TAG, "Issue saving session timeout value", e)
}
}
"registration_name" -> {
try {
integrationRepository.updateRegistration(DeviceRegistration(deviceName = value!!))
} catch (e: Exception) {
Log.e(TAG, "Issue updating registration with new device name", e)
}
}
else -> throw IllegalArgumentException("No string found by this key: $key")
}
}
}
override fun onFinish() {
mainScope.cancel()
}
override fun updateUrlStatus() {
mainScope.launch {
view.updateExternalUrl(
urlRepository.getUrl(false)?.toString() ?: "",
urlRepository.shouldUseCloud() && urlRepository.canUseCloud()
)
}
mainScope.launch {
val ssids = urlRepository.getHomeWifiSsids()
if (ssids.isEmpty()) urlRepository.saveUrl("", true)
view.enableInternalConnection(ssids.isNotEmpty())
view.updateSsids(ssids)
}
}
override fun isSsidUsed(): Boolean = runBlocking {
urlRepository.getHomeWifiSsids().isNotEmpty()
}
override fun clearSsids() {
mainScope.launch {
urlRepository.saveHomeWifiSsids(emptySet())
updateUrlStatus()
}
}
override fun setAppActive() = runBlocking {
integrationRepository.setAppActive(true)
}
}

View file

@ -0,0 +1,7 @@
package io.homeassistant.companion.android.settings.server
interface ServerSettingsView {
fun enableInternalConnection(isEnabled: Boolean)
fun updateExternalUrl(url: String, useCloud: Boolean)
fun updateSsids(ssids: Set<String>)
}

View file

@ -9,6 +9,7 @@ import dagger.hilt.android.qualifiers.ActivityContext
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.authentication.SessionState
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
import io.homeassistant.companion.android.matter.MatterFrontendCommissioningStatus
@ -38,6 +39,7 @@ class WebViewPresenterImpl @Inject constructor(
private val urlUseCase: UrlRepository,
private val authenticationUseCase: AuthenticationRepository,
private val integrationUseCase: IntegrationRepository,
private val prefsRepository: PrefsRepository,
private val matterUseCase: MatterManager
) : WebViewPresenter {
@ -144,28 +146,20 @@ class WebViewPresenterImpl @Inject constructor(
}
}
override fun isFullScreen(): Boolean {
return runBlocking {
integrationUseCase.isFullScreenEnabled()
}
override fun isFullScreen(): Boolean = runBlocking {
prefsRepository.isFullScreenEnabled()
}
override fun isKeepScreenOnEnabled(): Boolean {
return runBlocking {
integrationUseCase.isKeepScreenOnEnabled()
}
override fun isKeepScreenOnEnabled(): Boolean = runBlocking {
prefsRepository.isKeepScreenOnEnabled()
}
override fun isPinchToZoomEnabled(): Boolean {
return runBlocking {
integrationUseCase.isPinchToZoomEnabled()
}
override fun isPinchToZoomEnabled(): Boolean = runBlocking {
prefsRepository.isPinchToZoomEnabled()
}
override fun isWebViewDebugEnabled(): Boolean {
return runBlocking {
integrationUseCase.isWebViewDebugEnabled()
}
override fun isWebViewDebugEnabled(): Boolean = runBlocking {
prefsRepository.isWebViewDebugEnabled()
}
override fun isAppLocked(): Boolean {
@ -186,16 +180,12 @@ class WebViewPresenterImpl @Inject constructor(
}
}
override fun isAutoPlayVideoEnabled(): Boolean {
return runBlocking {
integrationUseCase.isAutoPlayVideoEnabled()
}
override fun isAutoPlayVideoEnabled(): Boolean = runBlocking {
prefsRepository.isAutoPlayVideoEnabled()
}
override fun isAlwaysShowFirstViewOnAppStartEnabled(): Boolean {
return runBlocking {
integrationUseCase.isAlwaysShowFirstViewOnAppStartEnabled()
}
override fun isAlwaysShowFirstViewOnAppStartEnabled(): Boolean = runBlocking {
prefsRepository.isAlwaysShowFirstViewOnAppStartEnabled()
}
override fun sessionTimeOut(): Int {

View file

@ -3,28 +3,16 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:title="@string/pref_connection_title">
android:key="servers_devices_category"
android:title="@string/servers_devices_category">
<!-- Servers will be added at runtime -->
<Preference
android:key="connection_external"
android:icon="@drawable/ic_globe"
android:title="@string/pref_connection_url"/>
<Preference
android:key="connection_internal_ssids"
android:icon="@drawable/ic_wifi"
android:title="@string/pref_connection_wifi"
android:summary="@string/pref_connection_ssids_empty" />
<EditTextPreference
android:key="connection_internal"
android:title="@string/pref_connection_internal"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/device_registration">
<EditTextPreference
android:key="registration_name"
android:icon="@drawable/ic_edit"
android:title="@string/device_name"
app:useSimpleSummaryProvider="true"/>
android:key="wear_settings"
app:isPreferenceVisible="false"
android:title="@string/wear_os_settings_title"
android:icon="@drawable/ic_baseline_watch_24"
android:order="999"
android:summary="@string/wear_os_settings_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/sensors">
@ -39,36 +27,6 @@
android:title="@string/sensor_update_frequency"
android:summary="@string/sensor_update_frequency_summary" />
</PreferenceCategory>
<PreferenceCategory
android:key="security_category"
android:title="@string/security">
<SwitchPreference
android:key="app_lock"
android:icon="@drawable/ic_lock"
android:title="@string/lock_title"
android:summary="@string/lock_summary"/>
<SwitchPreference
android:key="app_lock_home_bypass"
android:icon="@drawable/ic_wifi"
android:title="@string/lock_home_bypass_title"
android:summary="@string/lock_home_bypass_summary"/>
<EditTextPreference
android:key="session_timeout"
android:icon="@drawable/ic_timeout"
android:title="@string/session_timeout_title"
app:isPreferenceVisible="false"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
android:key="wear_category"
app:isPreferenceVisible="false"
android:title="@string/wear_os_category">
<Preference
android:key="wear_settings"
android:title="@string/wear_os_settings_title"
android:icon="@drawable/ic_baseline_watch_24"
android:summary="@string/wear_os_settings_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/other_settings">
<SwitchPreference
@ -123,11 +81,6 @@
<Preference
android:key="background"
android:title="@string/background_access_title"/>
<Preference
android:key="websocket"
android:icon="@drawable/ic_websocket"
android:title="@string/websocket_setting_name"
android:summary="@string/websocket_setting_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/notifications"

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:title="@string/pref_connection_title">
<Preference
android:key="connection_external"
android:icon="@drawable/ic_globe"
android:title="@string/pref_connection_url"/>
<Preference
android:key="connection_internal_ssids"
android:icon="@drawable/ic_wifi"
android:title="@string/pref_connection_wifi"
android:summary="@string/pref_connection_ssids_empty" />
<EditTextPreference
android:key="connection_internal"
android:title="@string/pref_connection_internal"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/device_registration">
<EditTextPreference
android:key="registration_name"
android:icon="@drawable/ic_edit"
android:title="@string/device_name"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
android:key="security_category"
android:title="@string/security">
<SwitchPreference
android:key="app_lock"
android:icon="@drawable/ic_lock"
android:title="@string/lock_title"
android:summary="@string/lock_summary"/>
<SwitchPreference
android:key="app_lock_home_bypass"
android:icon="@drawable/ic_wifi"
android:title="@string/lock_home_bypass_title"
android:summary="@string/lock_home_bypass_summary"/>
<EditTextPreference
android:key="session_timeout"
android:icon="@drawable/ic_timeout"
android:title="@string/session_timeout_title"
app:isPreferenceVisible="false"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/other_settings">
<Preference
android:key="websocket"
android:icon="@drawable/ic_websocket"
android:title="@string/websocket_setting_name"
android:summary="@string/websocket_setting_summary" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -53,6 +53,11 @@ class LocalStorageImpl(private val sharedPreferences: SharedPreferences) : Local
return sharedPreferences.getBoolean(key, false)
}
override suspend fun getBooleanOrNull(key: String): Boolean? =
if (sharedPreferences.contains(key)) {
sharedPreferences.getBoolean(key, false)
} else null
override suspend fun putStringSet(key: String, value: Set<String>) {
sharedPreferences.edit().putStringSet(key, value).apply()
}

View file

@ -24,6 +24,8 @@ import io.homeassistant.companion.android.common.data.keychain.KeyChainRepositor
import io.homeassistant.companion.android.common.data.keychain.KeyChainRepositoryImpl
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.prefs.PrefsRepositoryImpl
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepositoryImpl
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.data.url.UrlRepositoryImpl
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
@ -98,6 +100,17 @@ abstract class DataModule {
)
)
@Provides
@Named("wear")
@Singleton
fun provideWearPrefsLocalStorage(@ApplicationContext appContext: Context): LocalStorage =
LocalStorageImpl(
appContext.getSharedPreferences(
"wear_0",
Context.MODE_PRIVATE
)
)
@Provides
@Named("manufacturer")
@Singleton
@ -143,6 +156,10 @@ abstract class DataModule {
@Singleton
abstract fun bindPrefsRepository(prefsRepository: PrefsRepositoryImpl): PrefsRepository
@Binds
@Singleton
abstract fun bindWearPrefsRepository(wearPrefsRepository: WearPrefsRepositoryImpl): WearPrefsRepository
@Binds
@Singleton
abstract fun bindUrlRepository(urlRepository: UrlRepositoryImpl): UrlRepository

View file

@ -18,6 +18,8 @@ interface LocalStorage {
suspend fun getBoolean(key: String): Boolean
suspend fun getBooleanOrNull(key: String): Boolean?
suspend fun putStringSet(key: String, value: Set<String>)
suspend fun getStringSet(key: String): Set<String>?

View file

@ -21,24 +21,6 @@ interface IntegrationRepository {
suspend fun getZones(): Array<Entity<ZoneAttributes>>
suspend fun setFullScreenEnabled(enabled: Boolean)
suspend fun isFullScreenEnabled(): Boolean
suspend fun setKeepScreenOnEnabled(enabled: Boolean)
suspend fun isKeepScreenOnEnabled(): Boolean
suspend fun setPinchToZoomEnabled(enabled: Boolean)
suspend fun isPinchToZoomEnabled(): Boolean
suspend fun setAutoPlayVideo(enabled: Boolean)
suspend fun isAutoPlayVideoEnabled(): Boolean
suspend fun setAlwaysShowFirstViewOnAppStart(enabled: Boolean)
suspend fun isAlwaysShowFirstViewOnAppStartEnabled(): Boolean
suspend fun setWebViewDebugEnabled(enabled: Boolean)
suspend fun isWebViewDebugEnabled(): Boolean
suspend fun isAppLocked(): Boolean
suspend fun setAppActive(active: Boolean)
@ -47,24 +29,6 @@ interface IntegrationRepository {
suspend fun setSessionExpireMillis(value: Long)
suspend fun setControlsAuthRequired(setting: ControlsAuthRequiredSetting)
suspend fun getControlsAuthRequired(): ControlsAuthRequiredSetting
suspend fun setControlsAuthEntities(entities: List<String>)
suspend fun getControlsAuthEntities(): List<String>
suspend fun getTileShortcuts(): List<String>
suspend fun setTileShortcuts(entities: List<String>)
suspend fun getTemplateTileContent(): String
suspend fun setTemplateTileContent(content: String)
suspend fun getTemplateTileRefreshInterval(): Int
suspend fun setTemplateTileRefreshInterval(interval: Int)
suspend fun setWearHapticFeedback(enabled: Boolean)
suspend fun getWearHapticFeedback(): Boolean
suspend fun setWearToastConfirmation(enabled: Boolean)
suspend fun getWearToastConfirmation(): Boolean
suspend fun getShowShortcutText(): Boolean
suspend fun setShowShortcutTextEnabled(enabled: Boolean)
suspend fun getHomeAssistantVersion(): String
suspend fun isHomeAssistantVersionAtLeast(year: Int, month: Int, release: Int): Boolean

View file

@ -5,7 +5,6 @@ import io.homeassistant.companion.android.common.BuildConfig
import io.homeassistant.companion.android.common.data.HomeAssistantVersion
import io.homeassistant.companion.android.common.data.LocalStorage
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.integration.ControlsAuthRequiredSetting
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationException
@ -31,7 +30,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.json.JSONArray
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Named
@ -61,23 +59,9 @@ class IntegrationRepositoryImpl @Inject constructor(
private const val PREF_SECRET = "secret"
private const val PREF_CHECK_SENSOR_REGISTRATION_NEXT = "sensor_reg_last"
private const val PREF_TILE_SHORTCUTS = "tile_shortcuts_list"
private const val PREF_SHOW_TILE_SHORTCUTS_TEXT = "show_tile_shortcuts_text"
private const val PREF_TILE_TEMPLATE = "tile_template"
private const val PREF_TILE_TEMPLATE_REFRESH_INTERVAL = "tile_template_refresh_interval"
private const val PREF_WEAR_HAPTIC_FEEDBACK = "wear_haptic_feedback"
private const val PREF_WEAR_TOAST_CONFIRMATION = "wear_toast_confirmation"
private const val PREF_HA_VERSION = "ha_version"
private const val PREF_AUTOPLAY_VIDEO = "autoplay_video"
private const val PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START = "always_show_first_view_on_app_start"
private const val PREF_FULLSCREEN_ENABLED = "fullscreen_enabled"
private const val PREF_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled"
private const val PREF_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled"
private const val PREF_WEBVIEW_DEBUG_ENABLED = "webview_debug_enabled"
private const val PREF_SESSION_TIMEOUT = "session_timeout"
private const val PREF_SESSION_EXPIRE = "session_expire"
private const val PREF_CONTROLS_AUTH_REQUIRED = "controls_auth_required"
private const val PREF_CONTROLS_AUTH_ENTITIES = "controls_auth_entities"
private const val PREF_SEC_WARNING_NEXT = "sec_warning_last"
private const val TAG = "IntegrationRepository"
private const val RATE_LIMIT_URL = BuildConfig.RATE_LIMIT_URL
@ -338,54 +322,6 @@ class IntegrationRepositoryImpl @Inject constructor(
else throw IntegrationException("Error calling integration request get_zones")
}
override suspend fun setFullScreenEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_FULLSCREEN_ENABLED, enabled)
}
override suspend fun isFullScreenEnabled(): Boolean {
return localStorage.getBoolean(PREF_FULLSCREEN_ENABLED)
}
override suspend fun setKeepScreenOnEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_KEEP_SCREEN_ON_ENABLED, enabled)
}
override suspend fun isKeepScreenOnEnabled(): Boolean {
return localStorage.getBoolean(PREF_KEEP_SCREEN_ON_ENABLED)
}
override suspend fun setPinchToZoomEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_PINCH_TO_ZOOM_ENABLED, enabled)
}
override suspend fun isPinchToZoomEnabled(): Boolean {
return localStorage.getBoolean(PREF_PINCH_TO_ZOOM_ENABLED)
}
override suspend fun setWebViewDebugEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_WEBVIEW_DEBUG_ENABLED, enabled)
}
override suspend fun isWebViewDebugEnabled(): Boolean {
return localStorage.getBoolean(PREF_WEBVIEW_DEBUG_ENABLED)
}
override suspend fun isAutoPlayVideoEnabled(): Boolean {
return localStorage.getBoolean(PREF_AUTOPLAY_VIDEO)
}
override suspend fun setAlwaysShowFirstViewOnAppStart(enabled: Boolean) {
localStorage.putBoolean(PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START, enabled)
}
override suspend fun isAlwaysShowFirstViewOnAppStartEnabled(): Boolean {
return localStorage.getBoolean(PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START)
}
override suspend fun setAutoPlayVideo(enabled: Boolean) {
localStorage.putBoolean(PREF_AUTOPLAY_VIDEO, enabled)
}
override suspend fun isAppLocked(): Boolean {
val lockEnabled = authenticationRepository.isLockEnabled()
val sessionExpireMillis = getSessionExpireMillis()
@ -422,76 +358,6 @@ class IntegrationRepositoryImpl @Inject constructor(
return localStorage.getLong(PREF_SESSION_EXPIRE) ?: 0
}
override suspend fun setControlsAuthRequired(setting: ControlsAuthRequiredSetting) {
localStorage.putString(PREF_CONTROLS_AUTH_REQUIRED, setting.name)
}
override suspend fun getControlsAuthRequired(): ControlsAuthRequiredSetting {
val current = localStorage.getString(PREF_CONTROLS_AUTH_REQUIRED)
return ControlsAuthRequiredSetting.values().firstOrNull {
it.name == current
} ?: ControlsAuthRequiredSetting.NONE
}
override suspend fun setControlsAuthEntities(entities: List<String>) {
localStorage.putStringSet(PREF_CONTROLS_AUTH_ENTITIES, entities.toSet())
}
override suspend fun getControlsAuthEntities(): List<String> {
return localStorage.getStringSet(PREF_CONTROLS_AUTH_ENTITIES)?.toList() ?: emptyList()
}
override suspend fun getTileShortcuts(): List<String> {
val jsonArray = JSONArray(localStorage.getString(PREF_TILE_SHORTCUTS) ?: "[]")
return List(jsonArray.length()) {
jsonArray.getString(it)
}
}
override suspend fun setTileShortcuts(entities: List<String>) {
localStorage.putString(PREF_TILE_SHORTCUTS, JSONArray(entities).toString())
}
override suspend fun getTemplateTileContent(): String {
return localStorage.getString(PREF_TILE_TEMPLATE) ?: ""
}
override suspend fun setTemplateTileContent(content: String) {
localStorage.putString(PREF_TILE_TEMPLATE, content)
}
override suspend fun getTemplateTileRefreshInterval(): Int {
return localStorage.getInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL) ?: 0
}
override suspend fun setTemplateTileRefreshInterval(interval: Int) {
localStorage.putInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL, interval)
}
override suspend fun setWearHapticFeedback(enabled: Boolean) {
localStorage.putBoolean(PREF_WEAR_HAPTIC_FEEDBACK, enabled)
}
override suspend fun getWearHapticFeedback(): Boolean {
return localStorage.getBoolean(PREF_WEAR_HAPTIC_FEEDBACK)
}
override suspend fun setWearToastConfirmation(enabled: Boolean) {
localStorage.putBoolean(PREF_WEAR_TOAST_CONFIRMATION, enabled)
}
override suspend fun getWearToastConfirmation(): Boolean {
return localStorage.getBoolean(PREF_WEAR_TOAST_CONFIRMATION)
}
override suspend fun setShowShortcutTextEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT, enabled)
}
override suspend fun getShowShortcutText(): Boolean {
return localStorage.getBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT)
}
override suspend fun getNotificationRateLimits(): RateLimitResponse {
val pushToken = localStorage.getString(PREF_PUSH_TOKEN) ?: ""
val requestBody = RateLimitRequest(pushToken)

View file

@ -1,5 +1,7 @@
package io.homeassistant.companion.android.common.data.prefs
import io.homeassistant.companion.android.common.data.integration.ControlsAuthRequiredSetting
interface PrefsRepository {
suspend fun getAppVersion(): String?
@ -17,6 +19,38 @@ interface PrefsRepository {
suspend fun saveLocales(lang: String)
suspend fun getControlsAuthRequired(): ControlsAuthRequiredSetting
suspend fun setControlsAuthRequired(setting: ControlsAuthRequiredSetting)
suspend fun getControlsAuthEntities(): List<String>
suspend fun setControlsAuthEntities(entities: List<String>)
suspend fun isFullScreenEnabled(): Boolean
suspend fun setFullScreenEnabled(enabled: Boolean)
suspend fun isKeepScreenOnEnabled(): Boolean
suspend fun setKeepScreenOnEnabled(enabled: Boolean)
suspend fun isPinchToZoomEnabled(): Boolean
suspend fun setPinchToZoomEnabled(enabled: Boolean)
suspend fun isAutoPlayVideoEnabled(): Boolean
suspend fun setAutoPlayVideo(enabled: Boolean)
suspend fun isAlwaysShowFirstViewOnAppStartEnabled(): Boolean
suspend fun setAlwaysShowFirstViewOnAppStart(enabled: Boolean)
suspend fun isWebViewDebugEnabled(): Boolean
suspend fun setWebViewDebugEnabled(enabled: Boolean)
suspend fun isCrashReporting(): Boolean
suspend fun setCrashReporting(crashReportingEnabled: Boolean)

View file

@ -1,22 +1,70 @@
package io.homeassistant.companion.android.common.data.prefs
import io.homeassistant.companion.android.common.data.LocalStorage
import io.homeassistant.companion.android.common.data.integration.ControlsAuthRequiredSetting
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Named
class PrefsRepositoryImpl @Inject constructor(
@Named("themes") private val localStorage: LocalStorage
@Named("themes") private val localStorage: LocalStorage,
@Named("integration") private val integrationStorage: LocalStorage
) : PrefsRepository {
companion object {
private const val MIGRATION_PREF = "migration"
private const val MIGRATION_VERSION = 1
private const val PREF_VER = "version"
private const val PREF_THEME = "theme"
private const val PREF_LANG = "lang"
private const val PREF_LOCALES = "locales"
private const val PREF_CONTROLS_AUTH_REQUIRED = "controls_auth_required"
private const val PREF_CONTROLS_AUTH_ENTITIES = "controls_auth_entities"
private const val PREF_FULLSCREEN_ENABLED = "fullscreen_enabled"
private const val PREF_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled"
private const val PREF_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled"
private const val PREF_AUTOPLAY_VIDEO = "autoplay_video"
private const val PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START = "always_show_first_view_on_app_start"
private const val PREF_WEBVIEW_DEBUG_ENABLED = "webview_debug_enabled"
private const val PREF_KEY_ALIAS = "key-alias"
private const val PREF_CRASH_REPORTING_DISABLED = "crash_reporting"
}
init {
runBlocking {
val currentVersion = localStorage.getInt(MIGRATION_PREF)
if (currentVersion == null || currentVersion < 1) {
integrationStorage.getString(PREF_CONTROLS_AUTH_REQUIRED)?.let {
localStorage.putString(PREF_CONTROLS_AUTH_REQUIRED, it)
}
integrationStorage.getStringSet(PREF_CONTROLS_AUTH_ENTITIES)?.let {
localStorage.putStringSet(PREF_CONTROLS_AUTH_ENTITIES, it)
}
integrationStorage.getBooleanOrNull(PREF_FULLSCREEN_ENABLED)?.let {
localStorage.putBoolean(PREF_FULLSCREEN_ENABLED, it)
}
integrationStorage.getBooleanOrNull(PREF_KEEP_SCREEN_ON_ENABLED)?.let {
localStorage.putBoolean(PREF_KEEP_SCREEN_ON_ENABLED, it)
}
integrationStorage.getBooleanOrNull(PREF_PINCH_TO_ZOOM_ENABLED)?.let {
localStorage.putBoolean(PREF_PINCH_TO_ZOOM_ENABLED, it)
}
integrationStorage.getBooleanOrNull(PREF_AUTOPLAY_VIDEO)?.let {
localStorage.putBoolean(PREF_AUTOPLAY_VIDEO, it)
}
integrationStorage.getBooleanOrNull(PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START)?.let {
localStorage.putBoolean(PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START, it)
}
integrationStorage.getBooleanOrNull(PREF_WEBVIEW_DEBUG_ENABLED)?.let {
localStorage.putBoolean(PREF_WEBVIEW_DEBUG_ENABLED, it)
}
localStorage.putInt(MIGRATION_PREF, MIGRATION_VERSION)
}
}
}
override suspend fun getAppVersion(): String? {
return localStorage.getString(PREF_VER)
}
@ -49,6 +97,73 @@ class PrefsRepositoryImpl @Inject constructor(
localStorage.putString(PREF_LOCALES, locales)
}
override suspend fun getControlsAuthRequired(): ControlsAuthRequiredSetting {
val current = localStorage.getString(PREF_CONTROLS_AUTH_REQUIRED)
return ControlsAuthRequiredSetting.values().firstOrNull {
it.name == current
} ?: ControlsAuthRequiredSetting.NONE
}
override suspend fun setControlsAuthRequired(setting: ControlsAuthRequiredSetting) {
localStorage.putString(PREF_CONTROLS_AUTH_REQUIRED, setting.name)
}
override suspend fun getControlsAuthEntities(): List<String> {
return localStorage.getStringSet(PREF_CONTROLS_AUTH_ENTITIES)?.toList() ?: emptyList()
}
override suspend fun setControlsAuthEntities(entities: List<String>) {
localStorage.putStringSet(PREF_CONTROLS_AUTH_ENTITIES, entities.toSet())
}
override suspend fun isFullScreenEnabled(): Boolean {
return localStorage.getBoolean(PREF_FULLSCREEN_ENABLED)
}
override suspend fun setFullScreenEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_FULLSCREEN_ENABLED, enabled)
}
override suspend fun isKeepScreenOnEnabled(): Boolean {
return localStorage.getBoolean(PREF_KEEP_SCREEN_ON_ENABLED)
}
override suspend fun setKeepScreenOnEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_KEEP_SCREEN_ON_ENABLED, enabled)
}
override suspend fun isPinchToZoomEnabled(): Boolean {
return localStorage.getBoolean(PREF_PINCH_TO_ZOOM_ENABLED)
}
override suspend fun setPinchToZoomEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_PINCH_TO_ZOOM_ENABLED, enabled)
}
override suspend fun isAutoPlayVideoEnabled(): Boolean {
return localStorage.getBoolean(PREF_AUTOPLAY_VIDEO)
}
override suspend fun setAutoPlayVideo(enabled: Boolean) {
localStorage.putBoolean(PREF_AUTOPLAY_VIDEO, enabled)
}
override suspend fun isAlwaysShowFirstViewOnAppStartEnabled(): Boolean {
return localStorage.getBoolean(PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START)
}
override suspend fun setAlwaysShowFirstViewOnAppStart(enabled: Boolean) {
localStorage.putBoolean(PREF_ALWAYS_SHOW_FIRST_VIEW_ON_APP_START, enabled)
}
override suspend fun isWebViewDebugEnabled(): Boolean {
return localStorage.getBoolean(PREF_WEBVIEW_DEBUG_ENABLED)
}
override suspend fun setWebViewDebugEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_WEBVIEW_DEBUG_ENABLED, enabled)
}
override suspend fun isCrashReporting(): Boolean {
return !localStorage.getBoolean(PREF_CRASH_REPORTING_DISABLED)
}

View file

@ -0,0 +1,16 @@
package io.homeassistant.companion.android.common.data.prefs
interface WearPrefsRepository {
suspend fun getTileShortcuts(): List<String>
suspend fun setTileShortcuts(entities: List<String>)
suspend fun getShowShortcutText(): Boolean
suspend fun setShowShortcutTextEnabled(enabled: Boolean)
suspend fun getTemplateTileContent(): String
suspend fun setTemplateTileContent(content: String)
suspend fun getTemplateTileRefreshInterval(): Int
suspend fun setTemplateTileRefreshInterval(interval: Int)
suspend fun getWearHapticFeedback(): Boolean
suspend fun setWearHapticFeedback(enabled: Boolean)
suspend fun getWearToastConfirmation(): Boolean
suspend fun setWearToastConfirmation(enabled: Boolean)
}

View file

@ -0,0 +1,104 @@
package io.homeassistant.companion.android.common.data.prefs
import io.homeassistant.companion.android.common.data.LocalStorage
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import javax.inject.Inject
import javax.inject.Named
class WearPrefsRepositoryImpl @Inject constructor(
@Named("wear") private val localStorage: LocalStorage,
@Named("integration") private val integrationStorage: LocalStorage
) : WearPrefsRepository {
companion object {
private const val MIGRATION_PREF = "migration"
private const val MIGRATION_VERSION = 1
private const val PREF_TILE_SHORTCUTS = "tile_shortcuts_list"
private const val PREF_SHOW_TILE_SHORTCUTS_TEXT = "show_tile_shortcuts_text"
private const val PREF_TILE_TEMPLATE = "tile_template"
private const val PREF_TILE_TEMPLATE_REFRESH_INTERVAL = "tile_template_refresh_interval"
private const val PREF_WEAR_HAPTIC_FEEDBACK = "wear_haptic_feedback"
private const val PREF_WEAR_TOAST_CONFIRMATION = "wear_toast_confirmation"
}
init {
runBlocking {
val currentVersion = localStorage.getInt(MIGRATION_PREF)
if (currentVersion == null || currentVersion < 1) {
integrationStorage.getString(PREF_TILE_SHORTCUTS)?.let {
localStorage.putString(PREF_TILE_SHORTCUTS, it)
}
integrationStorage.getBooleanOrNull(PREF_SHOW_TILE_SHORTCUTS_TEXT)?.let {
localStorage.putBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT, it)
}
integrationStorage.getString(PREF_TILE_TEMPLATE)?.let {
localStorage.putString(PREF_TILE_TEMPLATE, it)
}
integrationStorage.getInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL)?.let {
localStorage.putInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL, it)
}
integrationStorage.getBooleanOrNull(PREF_WEAR_HAPTIC_FEEDBACK)?.let {
localStorage.putBoolean(PREF_WEAR_HAPTIC_FEEDBACK, it)
}
integrationStorage.getBooleanOrNull(PREF_WEAR_TOAST_CONFIRMATION)?.let {
localStorage.putBoolean(PREF_WEAR_TOAST_CONFIRMATION, it)
}
localStorage.putInt(MIGRATION_PREF, MIGRATION_VERSION)
}
}
}
override suspend fun getTileShortcuts(): List<String> {
val jsonArray = JSONArray(localStorage.getString(PREF_TILE_SHORTCUTS) ?: "[]")
return List(jsonArray.length()) {
jsonArray.getString(it)
}
}
override suspend fun setTileShortcuts(entities: List<String>) {
localStorage.putString(PREF_TILE_SHORTCUTS, JSONArray(entities).toString())
}
override suspend fun getTemplateTileContent(): String {
return localStorage.getString(PREF_TILE_TEMPLATE) ?: ""
}
override suspend fun getShowShortcutText(): Boolean {
return localStorage.getBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT)
}
override suspend fun setShowShortcutTextEnabled(enabled: Boolean) {
localStorage.putBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT, enabled)
}
override suspend fun setTemplateTileContent(content: String) {
localStorage.putString(PREF_TILE_TEMPLATE, content)
}
override suspend fun getTemplateTileRefreshInterval(): Int {
return localStorage.getInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL) ?: 0
}
override suspend fun setTemplateTileRefreshInterval(interval: Int) {
localStorage.putInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL, interval)
}
override suspend fun getWearHapticFeedback(): Boolean {
return localStorage.getBoolean(PREF_WEAR_HAPTIC_FEEDBACK)
}
override suspend fun setWearHapticFeedback(enabled: Boolean) {
localStorage.putBoolean(PREF_WEAR_HAPTIC_FEEDBACK, enabled)
}
override suspend fun getWearToastConfirmation(): Boolean {
return localStorage.getBoolean(PREF_WEAR_TOAST_CONFIRMATION)
}
override suspend fun setWearToastConfirmation(enabled: Boolean) {
localStorage.putBoolean(PREF_WEAR_TOAST_CONFIRMATION, enabled)
}
}

View file

@ -645,6 +645,8 @@
<string name="sensor">Sensor</string>
<string name="sensors_with_settings">The following sensors offer custom settings: %1$s</string>
<string name="sensors">Sensors</string>
<string name="server_settings">Server Settings</string>
<string name="servers_devices_category">Servers &amp; Devices</string>
<string name="service_call_failure">Unable to send service call</string>
<string name="session_timeout_title">Session TimeOut (in seconds)</string>
<string name="set_favorite">Set Favorites</string>
@ -818,7 +820,6 @@
<string name="view_password">View Password</string>
<string name="wait">Wait</string>
<string name="wear_favorite_entities">Favorite Entities</string>
<string name="wear_os_category">Wear OS</string>
<string name="wear_os_settings_summary">Manage Wear OS App</string>
<string name="wear_os_settings_title">Wear OS Settings</string>
<string name="wear_set_favorites">Select your favorite entities to appear at the top of the Wear home screen. You can also drag and drop to change the order in which they appear.</string>

View file

@ -7,6 +7,7 @@ import io.homeassistant.companion.android.common.data.authentication.SessionStat
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketState
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
@ -29,7 +30,8 @@ import io.homeassistant.companion.android.common.R as commonR
class HomePresenterImpl @Inject constructor(
private val authenticationUseCase: AuthenticationRepository,
private val integrationUseCase: IntegrationRepository,
private val webSocketUseCase: WebSocketRepository
private val webSocketUseCase: WebSocketRepository,
private val wearPrefsRepository: WearPrefsRepository
) : HomePresenter {
companion object {
@ -223,50 +225,50 @@ class HomePresenterImpl @Inject constructor(
}
override suspend fun getTileShortcuts(): List<SimplifiedEntity> {
return integrationUseCase.getTileShortcuts().map { SimplifiedEntity(it) }
return wearPrefsRepository.getTileShortcuts().map { SimplifiedEntity(it) }
}
override suspend fun setTileShortcuts(entities: List<SimplifiedEntity>) {
integrationUseCase.setTileShortcuts(entities.map { it.entityString })
wearPrefsRepository.setTileShortcuts(entities.map { it.entityString })
}
override suspend fun getWearHapticFeedback(): Boolean {
return integrationUseCase.getWearHapticFeedback()
return wearPrefsRepository.getWearHapticFeedback()
}
override suspend fun setWearHapticFeedback(enabled: Boolean) {
integrationUseCase.setWearHapticFeedback(enabled)
wearPrefsRepository.setWearHapticFeedback(enabled)
}
override suspend fun getWearToastConfirmation(): Boolean {
return integrationUseCase.getWearToastConfirmation()
return wearPrefsRepository.getWearToastConfirmation()
}
override suspend fun setWearToastConfirmation(enabled: Boolean) {
integrationUseCase.setWearToastConfirmation(enabled)
wearPrefsRepository.setWearToastConfirmation(enabled)
}
override suspend fun getShowShortcutText(): Boolean {
return integrationUseCase.getShowShortcutText()
return wearPrefsRepository.getShowShortcutText()
}
override suspend fun setShowShortcutTextEnabled(enabled: Boolean) {
integrationUseCase.setShowShortcutTextEnabled(enabled)
wearPrefsRepository.setShowShortcutTextEnabled(enabled)
}
override suspend fun getTemplateTileContent(): String {
return integrationUseCase.getTemplateTileContent()
return wearPrefsRepository.getTemplateTileContent()
}
override suspend fun setTemplateTileContent(content: String) {
integrationUseCase.setTemplateTileContent(content)
wearPrefsRepository.setTemplateTileContent(content)
}
override suspend fun getTemplateTileRefreshInterval(): Int {
return integrationUseCase.getTemplateTileRefreshInterval()
return wearPrefsRepository.getTemplateTileRefreshInterval()
}
override suspend fun setTemplateTileRefreshInterval(interval: Int) {
integrationUseCase.setTemplateTileRefreshInterval(interval)
wearPrefsRepository.setTemplateTileRefreshInterval(interval)
}
}

View file

@ -18,6 +18,7 @@ import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.wear.getAll
@ -44,6 +45,9 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
@Inject
lateinit var integrationUseCase: IntegrationRepository
@Inject
lateinit var wearPrefsRepository: WearPrefsRepository
@Inject
lateinit var favoritesDao: FavoritesDao
@ -76,8 +80,8 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
dataMap.putBoolean(KEY_IS_AUTHENTICATED, integrationUseCase.isRegistered())
dataMap.putString(KEY_SUPPORTED_DOMAINS, objectMapper.writeValueAsString(HomePresenterImpl.supportedDomains))
dataMap.putString(KEY_FAVORITES, objectMapper.writeValueAsString(currentFavorites))
dataMap.putString(KEY_TEMPLATE_TILE, integrationUseCase.getTemplateTileContent())
dataMap.putInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, integrationUseCase.getTemplateTileRefreshInterval())
dataMap.putString(KEY_TEMPLATE_TILE, wearPrefsRepository.getTemplateTileContent())
dataMap.putInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, wearPrefsRepository.getTemplateTileRefreshInterval())
setUrgent()
asPutDataRequest()
}
@ -153,7 +157,7 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
private fun saveTileTemplate(dataMap: DataMap) = mainScope.launch {
val content = dataMap.getString(KEY_TEMPLATE_TILE, "")
val interval = dataMap.getInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
integrationUseCase.setTemplateTileContent(content)
integrationUseCase.setTemplateTileRefreshInterval(interval)
wearPrefsRepository.setTemplateTileContent(content)
wearPrefsRepository.setTemplateTileRefreshInterval(interval)
}
}

View file

@ -35,7 +35,7 @@ import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
import io.homeassistant.companion.android.data.SimplifiedEntity
import io.homeassistant.companion.android.util.getIcon
import kotlinx.coroutines.CoroutineScope
@ -62,7 +62,7 @@ class ShortcutsTile : TileService() {
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
@Inject
lateinit var integrationUseCase: IntegrationRepository
lateinit var wearPrefsRepository: WearPrefsRepository
override fun onTileRequest(requestParams: TileRequest): ListenableFuture<Tile> =
serviceScope.future {
@ -77,7 +77,7 @@ class ShortcutsTile : TileService() {
}
val entities = getEntities()
val showLabels = integrationUseCase.getShowShortcutText()
val showLabels = wearPrefsRepository.getShowShortcutText()
Tile.Builder()
.setResourcesVersion(entities.toString())
@ -94,7 +94,7 @@ class ShortcutsTile : TileService() {
override fun onResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources> =
serviceScope.future {
val showLabels = integrationUseCase.getShowShortcutText()
val showLabels = wearPrefsRepository.getShowShortcutText()
val iconSize = if (showLabels) ICON_SIZE_SMALL else ICON_SIZE_FULL
val density = requestParams.deviceParameters!!.screenDensity
val iconSizePx = (iconSize * density).roundToInt()
@ -146,7 +146,7 @@ class ShortcutsTile : TileService() {
}
private suspend fun getEntities(): List<SimplifiedEntity> {
return integrationUseCase.getTileShortcuts().map { SimplifiedEntity(it) }
return wearPrefsRepository.getTileShortcuts().map { SimplifiedEntity(it) }
}
fun layout(entities: List<SimplifiedEntity>, showLabels: Boolean): LayoutElement = Column.Builder().apply {

View file

@ -38,6 +38,7 @@ import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -53,11 +54,14 @@ class TemplateTile : TileService() {
@Inject
lateinit var integrationUseCase: IntegrationRepository
@Inject
lateinit var wearPrefsRepository: WearPrefsRepository
override fun onTileRequest(requestParams: TileRequest): ListenableFuture<Tile> =
serviceScope.future {
val state = requestParams.state
if (state != null && state.lastClickableId == "refresh") {
if (integrationUseCase.getWearHapticFeedback()) {
if (wearPrefsRepository.getWearHapticFeedback()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = applicationContext.getSystemService<VibratorManager>()
val vibrator = vibratorManager?.defaultVibrator
@ -69,7 +73,7 @@ class TemplateTile : TileService() {
}
}
val template = integrationUseCase.getTemplateTileContent()
val template = wearPrefsRepository.getTemplateTileContent()
val renderedText = try {
integrationUseCase.renderTemplate(template, mapOf()).toString()
} catch (e: Exception) {
@ -82,7 +86,7 @@ class TemplateTile : TileService() {
Tile.Builder()
.setResourcesVersion("1")
.setFreshnessIntervalMillis(
integrationUseCase.getTemplateTileRefreshInterval().toLong() * 1000
wearPrefsRepository.getTemplateTileRefreshInterval().toLong() * 1000
)
.setTimeline(
Timeline.Builder().addTimelineEntry(

View file

@ -10,6 +10,7 @@ import android.os.VibratorManager
import androidx.core.content.getSystemService
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
import io.homeassistant.companion.android.home.HomePresenterImpl
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@ -20,12 +21,15 @@ class TileActionReceiver : BroadcastReceiver() {
@Inject
lateinit var integrationUseCase: IntegrationRepository
@Inject
lateinit var wearPrefsRepository: WearPrefsRepository
override fun onReceive(context: Context?, intent: Intent?) {
val entityId: String? = intent?.getStringExtra("entity_id")
if (entityId != null) {
runBlocking {
if (integrationUseCase.getWearHapticFeedback()) {
if (wearPrefsRepository.getWearHapticFeedback()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context?.getSystemService<VibratorManager>()
val vibrator = vibratorManager?.defaultVibrator