mirror of
https://github.com/home-assistant/android
synced 2024-07-22 10:54:12 +00:00
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:
parent
3568a69101
commit
4163e1465e
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue