Add Android 14 device controls panel (#3855)

* Prepare controls panel activity for Android 14

* Allow controls panel to work while locked

* UI to enable/disable controls panel

 - Disable panel by default
 - Add area to controls settings on Android 14 to enable/disable panel

* Panel server/path settings

 - Add setting to choose which server and path to use for the panel

* Remove transition animation for panel

* experience -> mode
This commit is contained in:
Joris Pelgröm 2023-09-13 03:47:57 +02:00 committed by GitHub
parent 3568a69101
commit 4163e1465e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 419 additions and 70 deletions

View file

@ -230,11 +230,21 @@
<service android:name=".controls.HaControlsProviderService"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true">
<meta-data
android:name="android.service.controls.META_DATA_PANEL_ACTIVITY"
android:value="${applicationId}/io.homeassistant.companion.android.controls.HaControlsPanelActivity" />
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService"/>
</intent-filter>
</service>
<activity android:name=".controls.HaControlsPanelActivity"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true"
android:resizeableActivity="true"
android:taskAffinity="io.homeassistant.companion.android.controls"
android:enabled="false" />
<receiver
android:name=".sensors.LocationSensorManager"
android:enabled="true"

View file

@ -0,0 +1,57 @@
package io.homeassistant.companion.android.controls
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.webview.WebViewActivity
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class HaControlsPanelActivity : AppCompatActivity() {
@Inject
lateinit var serverManager: ServerManager
@Inject
lateinit var prefsRepository: PrefsRepository
private var launched = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!serverManager.isRegistered()) {
finish()
return
}
lifecycleScope.launch {
val serverId = prefsRepository.getControlsPanelServer() ?: serverManager.getServer()?.id
val path = prefsRepository.getControlsPanelPath()
Log.d("HaControlsPanel", "Launching WebView…")
startActivity(
WebViewActivity.newInstance(
context = this@HaControlsPanelActivity,
path = path,
serverId = serverId
).apply {
putExtra(WebViewActivity.EXTRA_SHOW_WHEN_LOCKED, true)
}
)
overridePendingTransition(0, 0) // Disable activity start/stop animation
// The device controls panel can flicker if this activity finishes to quickly, so handle
// it in onPause instead to reduce this
launched = true
}
}
override fun onPause() {
super.onPause()
if (launched) finish()
}
}

View file

@ -171,6 +171,9 @@ class SettingsFragment(
return@setOnPreferenceClickListener true
}
val isAutomotive =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && requireContext().packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
if (Build.MODEL != "Quest") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
findPreference<PreferenceCategory>("shortcuts")?.let {
@ -198,7 +201,7 @@ class SettingsFragment(
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (!isAutomotive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
findPreference<PreferenceCategory>("device_controls")?.let {
it.isVisible = true
}
@ -325,7 +328,6 @@ class SettingsFragment(
return@setOnPreferenceClickListener true
}
val isAutomotive = requireContext().packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
findPreference<PreferenceCategory>("android_auto")?.let {
it.isVisible =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (BuildConfig.FLAVOR == "full" || isAutomotive)

View file

@ -36,15 +36,19 @@ class ManageControlsSettingsFragment : Fragment() {
setContent {
MdcTheme {
ManageControlsView(
panelEnabled = viewModel.panelEnabled,
authSetting = viewModel.authRequired,
authRequiredList = viewModel.authRequiredList,
entitiesLoaded = viewModel.entitiesLoaded,
entitiesList = viewModel.entitiesList,
panelSetting = viewModel.panelSetting,
serversList = serverManager.defaultServers,
defaultServer = serverManager.getServer()?.id ?: 0,
onSetPanelEnabled = viewModel::enablePanelForControls,
onSelectAll = { viewModel.setAuthSetting(ControlsAuthRequiredSetting.NONE) },
onSelectNone = { viewModel.setAuthSetting(ControlsAuthRequiredSetting.ALL) },
onSelectEntity = { entityId, serverId -> viewModel.toggleAuthForEntity(entityId, serverId) }
onSelectEntity = { entityId, serverId -> viewModel.toggleAuthForEntity(entityId, serverId) },
onSetPanelSetting = { path, serverId -> viewModel.setPanelConfig(path, serverId) }
)
}
}

View file

@ -1,6 +1,8 @@
package io.homeassistant.companion.android.settings.controls
import android.app.Application
import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.getValue
@ -16,6 +18,7 @@ import io.homeassistant.companion.android.common.data.integration.Entity
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.servers.ServerManager
import io.homeassistant.companion.android.controls.HaControlsPanelActivity
import io.homeassistant.companion.android.controls.HaControlsProviderService
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -27,9 +30,12 @@ import javax.inject.Inject
class ManageControlsViewModel @Inject constructor(
private val serverManager: ServerManager,
private val prefsRepository: PrefsRepository,
application: Application
private val application: Application
) : AndroidViewModel(application) {
var panelEnabled by mutableStateOf(false)
private set
var authRequired by mutableStateOf(ControlsAuthRequiredSetting.NONE)
private set
@ -40,8 +46,26 @@ class ManageControlsViewModel @Inject constructor(
val entitiesList = mutableStateMapOf<Int, List<Entity<*>>>()
var panelSetting by mutableStateOf<Pair<String?, Int>?>(null)
private set
init {
viewModelScope.launch {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
panelEnabled =
application.packageManager.getComponentEnabledSetting(
ComponentName(application, HaControlsPanelActivity::class.java)
) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
val panelServer = prefsRepository.getControlsPanelServer()
val panelPath = prefsRepository.getControlsPanelPath()
panelSetting = if (panelServer != null) {
Pair(panelPath, panelServer)
} else {
null
}
}
authRequired = prefsRepository.getControlsAuthRequired()
authRequiredList.addAll(prefsRepository.getControlsAuthEntities())
@ -118,4 +142,29 @@ class ManageControlsViewModel @Inject constructor(
prefsRepository.setControlsAuthEntities(authRequiredList.toList())
}
}
fun enablePanelForControls(enabled: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return
application.packageManager.setComponentEnabledSetting(
ComponentName(application, HaControlsPanelActivity::class.java),
if (enabled) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT // Default is disabled
},
PackageManager.DONT_KILL_APP
)
panelEnabled = enabled
if (panelSetting?.second == null) {
serverManager.getServer()?.id?.let { setPanelConfig("", it) }
}
}
fun setPanelConfig(path: String, serverId: Int) = viewModelScope.launch {
val cleanedPath = path.trim().takeIf { it.isNotBlank() }
prefsRepository.setControlsPanelServer(serverId)
prefsRepository.setControlsPanelPath(cleanedPath)
panelSetting = Pair(cleanedPath, serverId)
}
}

View file

@ -1,124 +1,257 @@
package io.homeassistant.companion.android.settings.controls.views
import android.os.Build
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Divider
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.integration.ControlsAuthRequiredSetting
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.common.data.integration.friendlyName
import io.homeassistant.companion.android.database.server.Server
import io.homeassistant.companion.android.util.compose.HaAlertWarning
import io.homeassistant.companion.android.util.compose.ServerExposedDropdownMenu
import io.homeassistant.companion.android.util.compose.getEntityDomainString
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun ManageControlsView(
panelEnabled: Boolean,
authSetting: ControlsAuthRequiredSetting,
authRequiredList: List<String>,
entitiesLoaded: Boolean,
entitiesList: Map<Int, List<Entity<*>>>,
panelSetting: Pair<String?, Int>?,
serversList: List<Server>,
defaultServer: Int,
onSetPanelEnabled: (Boolean) -> Unit,
onSelectAll: () -> Unit,
onSelectNone: () -> Unit,
onSelectEntity: (String, Int) -> Unit
onSelectEntity: (String, Int) -> Unit,
onSetPanelSetting: (String, Int) -> Unit
) {
var selectedServer by remember { mutableStateOf(defaultServer) }
var selectedServer by remember { mutableIntStateOf(defaultServer) }
val initialPanelEnabled by rememberSaveable { mutableStateOf(panelEnabled) }
var panelServer by remember(panelSetting?.second) { mutableIntStateOf(panelSetting?.second ?: defaultServer) }
var panelPath by remember(panelSetting?.first) { mutableStateOf(panelSetting?.first ?: "") }
LazyColumn(contentPadding = PaddingValues(vertical = 16.dp)) {
item {
Text(
text = stringResource(commonR.string.controls_setting_choose_setting),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
if (entitiesLoaded) {
if (entitiesList.isNotEmpty()) {
item {
Row(modifier = Modifier.padding(all = 16.dp)) {
OutlinedButton(
onClick = onSelectAll,
enabled = authSetting !== ControlsAuthRequiredSetting.NONE,
modifier = Modifier.weight(1f)
) {
Text(stringResource(commonR.string.controls_setting_choose_all))
}
Spacer(modifier = Modifier.width(16.dp))
OutlinedButton(
onClick = onSelectNone,
enabled = authSetting !== ControlsAuthRequiredSetting.ALL,
modifier = Modifier.weight(1f)
) {
Text(stringResource(commonR.string.controls_setting_choose_none))
}
}
}
if (serversList.size > 1) {
item {
ServerExposedDropdownMenu(
servers = serversList,
current = selectedServer,
onSelected = { selectedServer = it },
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
)
}
}
items(entitiesList[selectedServer]?.size ?: 0, key = { "$selectedServer.${entitiesList[selectedServer]?.get(it)?.entityId}" }) { index ->
val entity = entitiesList[selectedServer]?.get(index) as Entity<Map<String, Any>>
ManageControlsEntity(
entityName = (
entity.attributes["friendly_name"]
?: entity.entityId
) as String,
entityDomain = entity.domain,
selected = (
authSetting == ControlsAuthRequiredSetting.NONE ||
(
authSetting == ControlsAuthRequiredSetting.SELECTION &&
!authRequiredList.contains("$selectedServer.${entity.entityId}")
)
),
onClick = { onSelectEntity(entity.entityId, selectedServer) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
item {
Text(
text = stringResource(commonR.string.controls_setting_panel),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
item {
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp, bottom = 48.dp)
.height(IntrinsicSize.Min)
) {
ManageControlsModeButton(
isPanel = false,
selected = !panelEnabled,
onClick = { onSetPanelEnabled(false) },
modifier = Modifier.weight(0.5f)
)
}
} else {
item {
Text(
text = stringResource(commonR.string.controls_setting_choose_empty),
modifier = Modifier.padding(all = 16.dp),
fontStyle = FontStyle.Italic
Divider(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
ManageControlsModeButton(
isPanel = true,
selected = panelEnabled,
onClick = { onSetPanelEnabled(true) },
modifier = Modifier.weight(0.5f)
)
}
}
} else {
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || !panelEnabled) {
item {
Column(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(24.dp))
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
Text(
text = stringResource(commonR.string.controls_setting_choose_setting),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
if (entitiesLoaded) {
if (entitiesList.isNotEmpty()) {
item {
Row(modifier = Modifier.padding(all = 16.dp)) {
OutlinedButton(
onClick = onSelectAll,
enabled = authSetting !== ControlsAuthRequiredSetting.NONE,
modifier = Modifier.weight(1f)
) {
Text(stringResource(commonR.string.controls_setting_choose_all))
}
Spacer(modifier = Modifier.width(16.dp))
OutlinedButton(
onClick = onSelectNone,
enabled = authSetting !== ControlsAuthRequiredSetting.ALL,
modifier = Modifier.weight(1f)
) {
Text(stringResource(commonR.string.controls_setting_choose_none))
}
}
}
if (serversList.size > 1) {
item {
ServerExposedDropdownMenu(
servers = serversList,
current = selectedServer,
onSelected = { selectedServer = it },
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
)
}
}
items(entitiesList[selectedServer]?.size ?: 0, key = { "$selectedServer.${entitiesList[selectedServer]?.get(it)?.entityId}" }) { index ->
val entity = entitiesList[selectedServer]?.get(index) ?: return@items
ManageControlsEntity(
entityName = entity.friendlyName,
entityDomain = entity.domain,
selected = (
authSetting == ControlsAuthRequiredSetting.NONE ||
(
authSetting == ControlsAuthRequiredSetting.SELECTION &&
!authRequiredList.contains("$selectedServer.${entity.entityId}")
)
),
onClick = { onSelectEntity(entity.entityId, selectedServer) }
)
}
} else {
item {
Text(
text = stringResource(commonR.string.controls_setting_choose_empty),
modifier = Modifier.padding(all = 16.dp),
fontStyle = FontStyle.Italic
)
}
}
} else {
item {
Column(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(24.dp))
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}
}
}
} else {
if (!initialPanelEnabled) {
item {
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
HaAlertWarning(
message = stringResource(commonR.string.controls_setting_alert),
action = null,
onActionClicked = {}
)
}
}
}
item {
Text(
text = stringResource(commonR.string.controls_setting_dashboard_setting),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
if (serversList.size > 1) {
item {
ServerExposedDropdownMenu(
servers = serversList,
current = panelServer,
onSelected = { panelServer = it },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
)
}
}
item {
TextField(
value = panelPath,
onValueChange = { panelPath = it },
label = { Text(stringResource(id = R.string.lovelace_view_dashboard)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, autoCorrect = false, keyboardType = KeyboardType.Uri),
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp)
)
}
item {
Row(
modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)
) {
Button(
enabled = (
(
panelPath != panelSetting?.first &&
!(panelPath == "" && panelSetting != null && panelSetting.first == null)
) ||
panelServer != panelSetting.second
),
onClick = { onSetPanelSetting(panelPath, panelServer) }
) {
Text(stringResource(commonR.string.save))
}
}
}
}
@ -157,3 +290,49 @@ fun ManageControlsEntity(
}
}
}
@Composable
fun ManageControlsModeButton(
isPanel: Boolean,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.height(IntrinsicSize.Max)
.selectable(selected = selected, onClick = onClick)
) {
Column(
modifier = Modifier.padding(all = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
asset = if (isPanel) {
CommunityMaterial.Icon3.cmd_view_dashboard
} else {
CommunityMaterial.Icon.cmd_dip_switch
},
contentDescription = null,
modifier = Modifier.size(36.dp),
colorFilter = ColorFilter.tint(LocalContentColor.current)
)
Text(
text = stringResource(if (isPanel) commonR.string.lovelace else commonR.string.controls_setting_mode_builtin_title),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "${stringResource(if (isPanel) commonR.string.controls_setting_mode_panel_info else commonR.string.controls_setting_mode_builtin_info)}\n", // Newline for spacing
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
RadioButton(
selected = selected,
onClick = null // Handled by parent
)
}
}
}

View file

@ -124,6 +124,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
companion object {
const val EXTRA_PATH = "path"
const val EXTRA_SERVER = "server"
const val EXTRA_SHOW_WHEN_LOCKED = "show_when_locked"
private const val TAG = "WebviewActivity"
private const val APP_PREFIX = "app://"
@ -226,6 +227,14 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
if (
intent.extras?.containsKey(EXTRA_SHOW_WHEN_LOCKED) == true &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
) {
// Allow showing this on the lock screen when using device controls panel
setShowWhenLocked(intent.extras?.getBoolean(EXTRA_SHOW_WHEN_LOCKED) ?: false)
}
super.onCreate(savedInstanceState)
binding = ActivityWebviewBinding.inflate(layoutInflater)

View file

@ -27,6 +27,14 @@ interface PrefsRepository {
suspend fun setControlsAuthEntities(entities: List<String>)
suspend fun getControlsPanelServer(): Int?
suspend fun setControlsPanelServer(serverId: Int)
suspend fun getControlsPanelPath(): String?
suspend fun setControlsPanelPath(path: String?)
suspend fun isFullScreenEnabled(): Boolean
suspend fun setFullScreenEnabled(enabled: Boolean)

View file

@ -22,6 +22,8 @@ class PrefsRepositoryImpl @Inject constructor(
private const val PREF_SCREEN_ORIENTATION = "screen_orientation"
private const val PREF_CONTROLS_AUTH_REQUIRED = "controls_auth_required"
private const val PREF_CONTROLS_AUTH_ENTITIES = "controls_auth_entities"
private const val CONTROLS_PANEL_SERVER = "controls_panel_server"
private const val CONTROLS_PANEL_PATH = "controls_panel_path"
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"
@ -127,6 +129,24 @@ class PrefsRepositoryImpl @Inject constructor(
localStorage.putStringSet(PREF_CONTROLS_AUTH_ENTITIES, entities.toSet())
}
override suspend fun getControlsPanelServer(): Int? =
localStorage.getInt(CONTROLS_PANEL_SERVER)
override suspend fun setControlsPanelServer(serverId: Int) {
localStorage.putInt(CONTROLS_PANEL_SERVER, serverId)
}
override suspend fun getControlsPanelPath(): String? =
localStorage.getString(CONTROLS_PANEL_PATH)
override suspend fun setControlsPanelPath(path: String?) {
if (path.isNullOrBlank()) {
localStorage.remove(CONTROLS_PANEL_PATH)
} else {
localStorage.putString(CONTROLS_PANEL_PATH, path)
}
}
override suspend fun isFullScreenEnabled(): Boolean {
return localStorage.getBoolean(PREF_FULLSCREEN_ENABLED)
}
@ -213,5 +233,10 @@ class PrefsRepositoryImpl @Inject constructor(
val autoFavorites = getAutoFavorites().filter { it.split("-")[0].toIntOrNull() != serverId }
setAutoFavorites(autoFavorites)
if (getControlsPanelServer() == serverId) {
localStorage.remove(CONTROLS_PANEL_SERVER)
setControlsPanelPath(null)
}
}
}

View file

@ -153,11 +153,17 @@
<string name="continue_connect">Continue</string>
<string name="controls_setting_category">Device controls</string>
<string name="controls_setting_title">Manage device controls</string>
<string name="controls_setting_panel">Choose which device controls mode you would like to use:</string>
<string name="controls_setting_mode_builtin_title">Built-in</string>
<string name="controls_setting_mode_builtin_info">Easy to use, with advanced features like locked device settings and multiple servers support</string>
<string name="controls_setting_mode_panel_info">Use one of your Home Assistant dashboards to fully customize your controls</string>
<string name="controls_setting_summary">Choose if quick access device controls can be used when this device is locked</string>
<string name="controls_setting_choose_setting">Choose which entities you want to be able to control using the built-in device controls option when this device is locked:</string>
<string name="controls_setting_choose_all">Select all</string>
<string name="controls_setting_choose_none">Select none</string>
<string name="controls_setting_choose_empty">No entities available</string>
<string name="controls_setting_alert">If you previously used built-in device controls, you may need to remove all controls before the dashboard will be shown</string>
<string name="controls_setting_dashboard_setting">Enter the dashboard path you want to use. When left empty, the default dashboard will be used.</string>
<string name="covers">Covers</string>
<string name="crash_reporting_summary">Help the developers fix bugs and crashes by leaving this enabled. If the application crashes this will automatically generate and send a report. If you notice a crash also create an issue on GitHub!</string>
<string name="crash_reporting">Crash reporting</string>