Skip to content

Commit 85c26b3

Browse files
authored
Hide Add to widget on Automotive (#6041)
* Disable AddTo widgets for Meta Quest
1 parent a6ed21c commit 85c26b3

File tree

8 files changed

+123
-29
lines changed

8 files changed

+123
-29
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/HomeAssistantApplication.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import io.homeassistant.companion.android.sensors.SensorReceiver
3232
import io.homeassistant.companion.android.settings.language.LanguagesManager
3333
import io.homeassistant.companion.android.themes.NightModeManager
3434
import io.homeassistant.companion.android.util.LifecycleHandler
35+
import io.homeassistant.companion.android.util.QuestUtil
3536
import io.homeassistant.companion.android.util.initCrashSaving
3637
import io.homeassistant.companion.android.util.threadPolicyIgnoredViolationRules
3738
import io.homeassistant.companion.android.util.vmPolicyIgnoredViolationRules
@@ -152,7 +153,7 @@ open class HomeAssistantApplication :
152153
)
153154

154155
// Update Quest only sensors when the device is a Quest
155-
if (Build.MODEL == "Quest") {
156+
if (QuestUtil.isQuest) {
156157
ContextCompat.registerReceiver(
157158
this,
158159
sensorReceiver,

app/src/main/kotlin/io/homeassistant/companion/android/sensors/QuestSensorManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package io.homeassistant.companion.android.sensors
33
import android.content.Context
44
import android.content.Intent
55
import android.content.IntentFilter
6-
import android.os.Build
76
import androidx.core.content.ContextCompat
87
import io.homeassistant.companion.android.common.R as commonR
98
import io.homeassistant.companion.android.common.sensors.SensorManager
9+
import io.homeassistant.companion.android.util.QuestUtil
1010

1111
class QuestSensorManager : SensorManager {
1212
companion object {
@@ -39,7 +39,7 @@ class QuestSensorManager : SensorManager {
3939
}
4040

4141
override fun hasSensor(context: Context): Boolean {
42-
return Build.MODEL == "Quest"
42+
return QuestUtil.isQuest
4343
}
4444

4545
override suspend fun requestSensorUpdate(context: Context) {

app/src/main/kotlin/io/homeassistant/companion/android/settings/SettingsFragment.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import io.homeassistant.companion.android.settings.vehicle.ManageAndroidAutoSett
5151
import io.homeassistant.companion.android.settings.wear.SettingsWearActivity
5252
import io.homeassistant.companion.android.settings.wear.SettingsWearDetection
5353
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsSettingsFragment
54+
import io.homeassistant.companion.android.util.QuestUtil
5455
import io.homeassistant.companion.android.util.applyBottomSafeDrawingInsets
5556
import io.homeassistant.companion.android.webview.WebViewActivity
5657
import java.time.Instant
@@ -189,7 +190,7 @@ class SettingsFragment(private val presenter: SettingsPresenter, private val lan
189190

190191
findPreference<PreferenceCategory>("assist")?.isVisible = !isAutomotive
191192

192-
findPreference<PreferenceCategory>("widgets")?.isVisible = Build.MODEL != "Quest" && !isAutomotive
193+
findPreference<PreferenceCategory>("widgets")?.isVisible = !QuestUtil.isQuest && !isAutomotive
193194
findPreference<Preference>("manage_widgets")?.setOnPreferenceClickListener {
194195
parentFragmentManager.commit {
195196
replace(R.id.content, ManageWidgetsSettingsFragment::class.java, null)
@@ -198,7 +199,7 @@ class SettingsFragment(private val presenter: SettingsPresenter, private val lan
198199
return@setOnPreferenceClickListener true
199200
}
200201

201-
if (Build.MODEL != "Quest") {
202+
if (!QuestUtil.isQuest) {
202203
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
203204
findPreference<PreferenceCategory>("shortcuts")?.let {
204205
it.isVisible = true

app/src/main/kotlin/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package io.homeassistant.companion.android.settings.server
22

33
import android.content.Intent
44
import android.graphics.Color
5-
import android.os.Build
65
import android.os.Bundle
76
import android.os.Handler
87
import android.os.Looper
@@ -38,6 +37,7 @@ import io.homeassistant.companion.android.settings.SettingsActivity
3837
import io.homeassistant.companion.android.settings.ssid.SsidFragment
3938
import io.homeassistant.companion.android.settings.url.ExternalUrlFragment
4039
import io.homeassistant.companion.android.settings.websocket.WebsocketSettingFragment
40+
import io.homeassistant.companion.android.util.QuestUtil
4141
import io.homeassistant.companion.android.util.applyBottomSafeDrawingInsets
4242
import io.homeassistant.companion.android.webview.WebViewActivity
4343
import java.net.URLEncoder
@@ -194,7 +194,7 @@ class ServerSettingsFragment :
194194
}
195195
}
196196

197-
findPreference<PreferenceCategory>("security_category")?.isVisible = Build.MODEL != "Quest"
197+
findPreference<PreferenceCategory>("security_category")?.isVisible = !QuestUtil.isQuest
198198

199199
findPreference<Preference>("websocket")?.let {
200200
it.setOnPreferenceClickListener {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.homeassistant.companion.android.util
2+
3+
import android.os.Build
4+
5+
/**
6+
* Utility for detecting Meta Quest devices.
7+
*/
8+
object QuestUtil {
9+
/**
10+
* Checks if the current device is a Meta Quest device.
11+
*
12+
* @return `true` if the device is a Meta Quest, `false` otherwise
13+
*/
14+
val isQuest: Boolean by lazy { Build.MODEL == "Quest" }
15+
}

app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ class WebViewActivity :
925925
val entityId = payload?.getStringOrNull("entity_id")
926926
entityId?.let {
927927
lifecycleScope.launch {
928-
val actions = entityAddToHandler.actionsForEntity(entityId)
928+
val actions = entityAddToHandler.actionsForEntity(this@WebViewActivity, entityId)
929929
sendExternalBusMessage(
930930
EntityAddToActionsResponse(
931931
id = json["id"],

app/src/main/kotlin/io/homeassistant/companion/android/webview/addto/EntityAddToHandler.kt

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.homeassistant.companion.android.webview.addto
22

33
import android.content.Context
4+
import androidx.annotation.VisibleForTesting
45
import io.homeassistant.companion.android.common.data.integration.IntegrationDomains.CAMERA_DOMAIN
56
import io.homeassistant.companion.android.common.data.integration.IntegrationDomains.IMAGE_DOMAIN
67
import io.homeassistant.companion.android.common.data.integration.IntegrationDomains.MEDIA_PLAYER_DOMAIN
@@ -10,6 +11,8 @@ import io.homeassistant.companion.android.common.data.prefs.AutoFavorite
1011
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
1112
import io.homeassistant.companion.android.common.data.servers.ServerManager
1213
import io.homeassistant.companion.android.common.util.FailFast
14+
import io.homeassistant.companion.android.common.util.isAutomotive
15+
import io.homeassistant.companion.android.util.QuestUtil
1316
import io.homeassistant.companion.android.util.vehicle.isVehicleDomain
1417
import io.homeassistant.companion.android.widgets.camera.CameraWidgetConfigureActivity
1518
import io.homeassistant.companion.android.widgets.entity.EntityWidgetConfigureActivity
@@ -43,33 +46,41 @@ class EntityAddToHandler @Inject constructor(
4346
* Vehicle-related entities will include Android Auto favorites. Camera and image entities
4447
* will include camera widget options.
4548
*
49+
* @param context An android context
4650
* @param entityId The entity ID to get available actions for (for example, "light.living_room")
4751
* @return List of actions that can be performed for this entity. Returns an empty list if the
4852
* entity is not found or if the server is unavailable.
4953
*/
50-
suspend fun actionsForEntity(entityId: String): List<EntityAddToAction> {
54+
suspend fun actionsForEntity(context: Context, entityId: String): List<EntityAddToAction> {
55+
return actionsForEntity(isAutomotive = context.isAutomotive(), isQuest = QuestUtil.isQuest, entityId)
56+
}
57+
58+
@VisibleForTesting
59+
suspend fun actionsForEntity(isAutomotive: Boolean, isQuest: Boolean, entityId: String): List<EntityAddToAction> {
5160
return withContext(Dispatchers.Default) {
5261
val actions = mutableListOf<EntityAddToAction>()
5362
serverManager.getServer()?.let { server ->
5463
serverManager.integrationRepository(server.id).getEntity(entityId)
5564
?.let { entity ->
56-
actions.add(EntityAddToAction.EntityWidget)
65+
if (!isAutomotive && !isQuest) {
66+
actions.add(EntityAddToAction.EntityWidget)
5767

58-
if (isVehicleDomain(entity)) {
59-
// We could check if it already exist but the action won't do anything so we can keep it
60-
actions.add(EntityAddToAction.AndroidAutoFavorite)
61-
}
68+
if (entity.domain == MEDIA_PLAYER_DOMAIN) {
69+
actions.add(EntityAddToAction.MediaPlayerWidget)
70+
}
6271

63-
if (entity.domain == MEDIA_PLAYER_DOMAIN) {
64-
actions.add(EntityAddToAction.MediaPlayerWidget)
65-
}
72+
if (entity.domain == TODO_DOMAIN) {
73+
actions.add(EntityAddToAction.TodoWidget)
74+
}
6675

67-
if (entity.domain == TODO_DOMAIN) {
68-
actions.add(EntityAddToAction.TodoWidget)
76+
if (entity.domain == CAMERA_DOMAIN || entity.domain == IMAGE_DOMAIN) {
77+
actions.add(EntityAddToAction.CameraWidget)
78+
}
6979
}
7080

71-
if (entity.domain == CAMERA_DOMAIN || entity.domain == IMAGE_DOMAIN) {
72-
actions.add(EntityAddToAction.CameraWidget)
81+
if (isVehicleDomain(entity)) {
82+
// We could check if it already exist but the action won't do anything so we can keep it
83+
actions.add(EntityAddToAction.AndroidAutoFavorite)
7384
}
7485
}
7586
}

app/src/test/kotlin/io/homeassistant/companion/android/webview/addto/EntityAddToHandlerTest.kt

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import org.junit.jupiter.api.Test
3434
import org.junit.jupiter.api.assertNotNull
3535
import org.junit.jupiter.api.extension.ExtendWith
3636
import org.junit.jupiter.api.fail
37+
import org.junit.jupiter.params.ParameterizedTest
38+
import org.junit.jupiter.params.provider.ValueSource
3739

3840
@ExperimentalCoroutinesApi
3941
@ExtendWith(ConsoleLogExtension::class)
@@ -102,7 +104,7 @@ class EntityAddToHandlerTest {
102104
val entityId = "light.test"
103105
coEvery { serverManager.getServer() } returns null
104106

105-
val actions = handler.actionsForEntity(entityId)
107+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId = entityId)
106108

107109
assertEquals(emptyList<EntityAddToAction>(), actions)
108110
}
@@ -112,7 +114,7 @@ class EntityAddToHandlerTest {
112114
val entityId = "light.nonexistent"
113115
coEvery { integrationRepository.getEntity(entityId) } returns null
114116

115-
val actions = handler.actionsForEntity(entityId)
117+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId)
116118

117119
assertEquals(emptyList<EntityAddToAction>(), actions)
118120
}
@@ -123,7 +125,7 @@ class EntityAddToHandlerTest {
123125

124126
mockGetEntity(entityId)
125127

126-
val actions = handler.actionsForEntity(entityId)
128+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId)
127129
assertEquals(1, actions.size)
128130
assertEquals(EntityAddToAction.EntityWidget, actions.first())
129131
}
@@ -134,7 +136,7 @@ class EntityAddToHandlerTest {
134136

135137
mockGetEntity(entityId)
136138

137-
val actions = handler.actionsForEntity(entityId)
139+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId)
138140

139141
assertEquals(listOf(EntityAddToAction.EntityWidget, EntityAddToAction.AndroidAutoFavorite), actions)
140142
}
@@ -145,7 +147,7 @@ class EntityAddToHandlerTest {
145147

146148
mockGetEntity(entityId)
147149

148-
val actions = handler.actionsForEntity(entityId)
150+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId)
149151

150152
assertEquals(listOf(EntityAddToAction.EntityWidget, EntityAddToAction.MediaPlayerWidget), actions)
151153
}
@@ -156,7 +158,7 @@ class EntityAddToHandlerTest {
156158

157159
mockGetEntity(entityId)
158160

159-
val actions = handler.actionsForEntity(entityId)
161+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId)
160162

161163
assertEquals(listOf(EntityAddToAction.EntityWidget, EntityAddToAction.TodoWidget), actions)
162164
}
@@ -167,7 +169,7 @@ class EntityAddToHandlerTest {
167169

168170
mockGetEntity(entityId)
169171

170-
val actions = handler.actionsForEntity(entityId)
172+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId)
171173

172174
assertEquals(listOf(EntityAddToAction.EntityWidget, EntityAddToAction.CameraWidget), actions)
173175
}
@@ -178,7 +180,7 @@ class EntityAddToHandlerTest {
178180

179181
mockGetEntity(entityId)
180182

181-
val actions = handler.actionsForEntity(entityId)
183+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = false, entityId)
182184
assertEquals(listOf(EntityAddToAction.EntityWidget, EntityAddToAction.CameraWidget), actions)
183185
}
184186

@@ -237,6 +239,70 @@ class EntityAddToHandlerTest {
237239
verify { TodoWidgetConfigureActivity.newInstance(context, entityId) }
238240
}
239241

242+
@ParameterizedTest
243+
@ValueSource(
244+
strings = [
245+
"light.test",
246+
"alarm_control_panel.test",
247+
],
248+
)
249+
fun `Given standard entity on automotive when getting actionsForEntity then returns auto favorite`(entityId: String) = runTest {
250+
mockGetEntity(entityId)
251+
252+
val actions = handler.actionsForEntity(isAutomotive = true, isQuest = false, entityId)
253+
254+
assertEquals(listOf(EntityAddToAction.AndroidAutoFavorite), actions)
255+
}
256+
257+
@ParameterizedTest
258+
@ValueSource(
259+
strings = [
260+
"$MEDIA_PLAYER_DOMAIN.test",
261+
"$TODO_DOMAIN.test",
262+
"$CAMERA_DOMAIN.test",
263+
"$IMAGE_DOMAIN.test",
264+
],
265+
)
266+
fun `Given entity on automotive not vehicle domain with when getting actionsForEntity then returns empty list`(entityId: String) = runTest {
267+
mockGetEntity(entityId)
268+
269+
val actions = handler.actionsForEntity(isAutomotive = true, isQuest = false, entityId)
270+
271+
assertEquals(emptyList<EntityAddToAction>(), actions)
272+
}
273+
274+
@ParameterizedTest
275+
@ValueSource(
276+
strings = [
277+
"light.test",
278+
"alarm_control_panel.test",
279+
],
280+
)
281+
fun `Given standard entity on Quest when getting actionsForEntity then returns auto favorite`(entityId: String) = runTest {
282+
mockGetEntity(entityId)
283+
284+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = true, entityId)
285+
286+
assertEquals(listOf(EntityAddToAction.AndroidAutoFavorite), actions)
287+
}
288+
289+
@ParameterizedTest
290+
@ValueSource(
291+
strings = [
292+
"$MEDIA_PLAYER_DOMAIN.test",
293+
"$TODO_DOMAIN.test",
294+
"$CAMERA_DOMAIN.test",
295+
"$IMAGE_DOMAIN.test",
296+
],
297+
)
298+
fun `Given entity on Quest not vehicle domain with when getting actionsForEntity then returns empty list`(entityId: String) = runTest {
299+
mockGetEntity(entityId)
300+
301+
val actions = handler.actionsForEntity(isAutomotive = false, isQuest = true, entityId)
302+
303+
assertEquals(emptyList<EntityAddToAction>(), actions)
304+
}
305+
240306
private fun mockGetEntity(entityId: String) {
241307
coEvery { integrationRepository.getEntity(entityId) } coAnswers {
242308
delay(1)

0 commit comments

Comments
 (0)