Skip to content

Commit 3e45a9c

Browse files
authored
More actions to configure for gestures (#5538)
* Match available actions on iOS except navigate back * Add reload page action * Add go to default dashboard action - Add categories to new actions - Adjust code for quickbar actions to also work if the current field is a text input - Add link to docs * Add screenshot tests for main screens * Server list > Servers list, for consistency with iOS * Changelog update * Deeplink to settings with enums - Update deeplinking into settings to use enums and a different extra for an optional item id, instead of a path-like extra
1 parent aea2bcf commit 3e45a9c

File tree

15 files changed

+206
-63
lines changed

15 files changed

+206
-63
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,7 +1655,7 @@ class MessagingManager @Inject constructor(
16551655

16561656
uri.startsWith(SETTINGS_PREFIX) -> {
16571657
if (uri.substringAfter(SETTINGS_PREFIX) == NOTIFICATION_HISTORY) {
1658-
SettingsActivity.newInstance(context)
1658+
SettingsActivity.newInstance(context, SettingsActivity.Deeplink.NOTIFICATION_HISTORY)
16591659
} else {
16601660
WebViewActivity.newInstance(context, null, serverId)
16611661
}
@@ -1676,10 +1676,6 @@ class MessagingManager @Inject constructor(
16761676
}
16771677
} ?: WebViewActivity.newInstance(context, null, serverId)
16781678

1679-
if (uri.startsWith(SETTINGS_PREFIX) && uri.substringAfter(SETTINGS_PREFIX) == NOTIFICATION_HISTORY) {
1680-
intent.putExtra("fragment", NOTIFICATION_HISTORY)
1681-
}
1682-
16831679
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
16841680
if (!otherApp) {
16851681
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)

app/src/main/kotlin/io/homeassistant/companion/android/qs/TileExtensions.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,11 @@ abstract class TileExtensions : TileService() {
255255
}
256256
} else {
257257
Timber.d("No tile data found for tile ID: $tileId")
258-
val tileSettingIntent = SettingsActivity.newInstance(context).apply {
259-
putExtra("fragment", "tiles/$tileId")
258+
val tileSettingIntent = SettingsActivity.newInstance(
259+
context,
260+
SettingsActivity.Deeplink.QS_TILE,
261+
tileId,
262+
).apply {
260263
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
261264
addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
262265
}

app/src/main/kotlin/io/homeassistant/companion/android/qs/TilePreferenceActivity.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@ class TilePreferenceActivity : BaseActivity() {
6767
serverId = tileData.serverId,
6868
)
6969
} else {
70-
SettingsActivity.newInstance(this@TilePreferenceActivity).apply {
71-
putExtra("fragment", "tiles/$tileId")
72-
}
70+
SettingsActivity.newInstance(this@TilePreferenceActivity, SettingsActivity.Deeplink.QS_TILE, tileId)
7371
}
7472

7573
withContext(Dispatchers.Main) {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,7 @@ class SensorReceiver : SensorReceiverBase() {
138138
sensorManagerId: String,
139139
notificationId: Int,
140140
): PendingIntent? {
141-
val intent = SettingsActivity.newInstance(context).apply {
142-
putExtra("fragment", "sensors/$sensorId")
141+
val intent = SettingsActivity.newInstance(context, SettingsActivity.Deeplink.SENSOR, sensorId).apply {
143142
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
144143
addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
145144
}

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

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.view.MenuItem
77
import android.view.View
88
import android.view.ViewGroup
99
import androidx.biometric.BiometricManager
10+
import androidx.core.content.IntentCompat
1011
import androidx.fragment.app.commit
1112
import dagger.hilt.EntryPoint
1213
import dagger.hilt.InstallIn
@@ -19,6 +20,7 @@ import io.homeassistant.companion.android.R
1920
import io.homeassistant.companion.android.authenticator.Authenticator
2021
import io.homeassistant.companion.android.common.R as commonR
2122
import io.homeassistant.companion.android.common.data.servers.ServerManager
23+
import io.homeassistant.companion.android.settings.developer.DeveloperSettingsFragment
2224
import io.homeassistant.companion.android.settings.notification.NotificationHistoryFragment
2325
import io.homeassistant.companion.android.settings.qs.ManageTilesFragment
2426
import io.homeassistant.companion.android.settings.sensor.SensorDetailFragment
@@ -29,6 +31,9 @@ import javax.inject.Inject
2931
import kotlinx.coroutines.runBlocking
3032
import timber.log.Timber
3133

34+
private const val EXTRA_FRAGMENT = "fragment"
35+
private const val EXTRA_FRAGMENT_ITEM = "fragment_item_id"
36+
3237
@AndroidEntryPoint
3338
class SettingsActivity : BaseActivity() {
3439

@@ -42,11 +47,26 @@ class SettingsActivity : BaseActivity() {
4247
private var externalAuthCallback: ((Int) -> Boolean)? = null
4348

4449
companion object {
45-
fun newInstance(context: Context): Intent {
46-
return Intent(context, SettingsActivity::class.java)
50+
fun newInstance(context: Context, screen: Deeplink? = null, screenItemId: String? = null): Intent {
51+
return Intent(context, SettingsActivity::class.java).apply {
52+
if (screen != null) {
53+
putExtra(EXTRA_FRAGMENT, screen)
54+
if (!screenItemId.isNullOrBlank()) {
55+
putExtra(EXTRA_FRAGMENT_ITEM, screenItemId)
56+
}
57+
}
58+
}
4759
}
4860
}
4961

62+
enum class Deeplink {
63+
DEVELOPER,
64+
NOTIFICATION_HISTORY,
65+
QS_TILE,
66+
SENSOR,
67+
WEBSOCKET,
68+
}
69+
5070
override fun onCreate(savedInstanceState: Bundle?) {
5171
val entryPoint = EntryPointAccessors.fromActivity(this, SettingsFragmentFactoryEntryPoint::class.java)
5272
supportFragmentManager.fragmentFactory = entryPoint.getSettingsFragmentFactory()
@@ -68,30 +88,29 @@ class SettingsActivity : BaseActivity() {
6888
authenticator = Authenticator(this, this, ::settingsActivityAuthenticationResult)
6989

7090
if (savedInstanceState == null) {
71-
val settingsNavigation = intent.getStringExtra("fragment")
91+
val settingsNavigation = IntentCompat.getSerializableExtra(intent, EXTRA_FRAGMENT, Deeplink::class.java)
7292
supportFragmentManager.commit {
7393
replace(
7494
R.id.content,
75-
when {
76-
settingsNavigation == "websocket" ->
77-
if (serverManager.defaultServers.size == 1) {
78-
WebsocketSettingFragment::class.java
79-
} else {
80-
SettingsFragment::class.java
81-
}
82-
83-
settingsNavigation == "notification_history" -> NotificationHistoryFragment::class.java
84-
settingsNavigation?.startsWith("sensors/") == true -> SensorDetailFragment::class.java
85-
settingsNavigation?.startsWith("tiles/") == true -> ManageTilesFragment::class.java
95+
when (settingsNavigation) {
96+
Deeplink.WEBSOCKET -> if (serverManager.defaultServers.size == 1) {
97+
WebsocketSettingFragment::class.java
98+
} else {
99+
SettingsFragment::class.java
100+
}
101+
Deeplink.DEVELOPER -> DeveloperSettingsFragment::class.java
102+
Deeplink.NOTIFICATION_HISTORY -> NotificationHistoryFragment::class.java
103+
Deeplink.SENSOR -> SensorDetailFragment::class.java
104+
Deeplink.QS_TILE -> ManageTilesFragment::class.java
86105
else -> SettingsFragment::class.java
87106
},
88-
if (settingsNavigation?.startsWith("sensors/") == true) {
89-
val sensorId = settingsNavigation.split("/")[1]
107+
if (settingsNavigation == Deeplink.SENSOR) {
108+
val sensorId = intent.getStringExtra(EXTRA_FRAGMENT_ITEM) ?: ""
90109
SensorDetailFragment.newInstance(sensorId).arguments
91-
} else if (settingsNavigation?.startsWith("tiles/") == true) {
92-
val tileId = settingsNavigation.split("/")[1]
110+
} else if (settingsNavigation == Deeplink.QS_TILE) {
111+
val tileId = intent.getStringExtra(EXTRA_FRAGMENT_ITEM) ?: ""
93112
Bundle().apply { putString("id", tileId) }
94-
} else if (settingsNavigation == "websocket") {
113+
} else if (settingsNavigation == Deeplink.WEBSOCKET) {
95114
val servers = serverManager.defaultServers
96115
if (servers.size == 1) {
97116
Bundle().apply { putInt(WebsocketSettingFragment.EXTRA_SERVER, servers[0].id) }

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.compose.ui.platform.ComposeView
88
import androidx.fragment.app.Fragment
99
import androidx.fragment.app.viewModels
1010
import dagger.hilt.android.AndroidEntryPoint
11+
import io.homeassistant.companion.android.settings.addHelpMenuProvider
1112
import io.homeassistant.companion.android.settings.gestures.views.GesturesScreen
1213
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
1314
import kotlin.getValue
@@ -32,4 +33,8 @@ class GesturesFragment : Fragment() {
3233
}
3334
}
3435
}
36+
37+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38+
addHelpMenuProvider("https://companion.home-assistant.io/docs/integrations/gestures")
39+
}
3540
}

app/src/main/kotlin/io/homeassistant/companion/android/websocket/WebsocketManager.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,7 @@ class WebsocketManager(appContext: Context, workerParams: WorkerParameters) :
240240
PendingIntent.FLAG_IMMUTABLE,
241241
)
242242

243-
val settingIntent = SettingsActivity.newInstance(applicationContext)
244-
settingIntent.putExtra("fragment", "websocket")
243+
val settingIntent = SettingsActivity.newInstance(applicationContext, SettingsActivity.Deeplink.WEBSOCKET)
245244
settingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
246245
settingIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
247246
settingIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)

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

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import io.homeassistant.companion.android.websocket.WebsocketManager
107107
import io.homeassistant.companion.android.webview.WebView.ErrorType
108108
import io.homeassistant.companion.android.webview.externalbus.ExternalBusMessage
109109
import io.homeassistant.companion.android.webview.externalbus.NavigateTo
110+
import io.homeassistant.companion.android.webview.externalbus.ShowSidebar
110111
import javax.inject.Inject
111112
import javax.inject.Named
112113
import kotlinx.coroutines.CoroutineScope
@@ -898,8 +899,41 @@ class WebViewActivity :
898899
private fun handleWebViewGesture(direction: GestureDirection, pointerCount: Int) {
899900
lifecycleScope.launch {
900901
when (presenter.getGestureAction(direction, pointerCount)) {
901-
GestureAction.SERVER_NEXT -> presenter.nextServer()
902-
GestureAction.SERVER_PREVIOUS -> presenter.previousServer()
902+
GestureAction.NONE -> {
903+
// Do nothing
904+
}
905+
906+
GestureAction.QUICKBAR_DEFAULT -> {
907+
webView.dispatchKeyDownEventToDocument("e", "KeyE", 69)
908+
}
909+
910+
GestureAction.QUICKBAR_DEVICES -> {
911+
webView.dispatchKeyDownEventToDocument("d", "KeyD", 68)
912+
}
913+
914+
GestureAction.QUICKBAR_COMMANDS -> {
915+
webView.dispatchKeyDownEventToDocument("c", "KeyC", 67)
916+
}
917+
918+
GestureAction.SHOW_SIDEBAR -> sendExternalBusMessage(ShowSidebar)
919+
920+
GestureAction.OPEN_ASSIST -> startActivity(
921+
AssistActivity.newInstance(
922+
this@WebViewActivity,
923+
serverId = presenter.getActiveServer(),
924+
),
925+
)
926+
927+
GestureAction.NAVIGATE_FORWARD -> {
928+
if (webView.canGoForward()) webView.goForward()
929+
}
930+
931+
GestureAction.NAVIGATE_DASHBOARD -> navigateToDefaultDashboard()
932+
933+
GestureAction.NAVIGATE_RELOAD -> {
934+
webView.reload()
935+
}
936+
903937
GestureAction.SERVER_LIST -> {
904938
val serverChooser = ServerChooserFragment()
905939
supportFragmentManager.setFragmentResultListener(
@@ -919,13 +953,18 @@ class WebViewActivity :
919953
serverChooser.show(supportFragmentManager, ServerChooserFragment.TAG)
920954
}
921955

922-
GestureAction.QUICKBAR_DEFAULT -> {
923-
webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_E))
924-
}
956+
GestureAction.SERVER_NEXT -> presenter.nextServer()
925957

926-
GestureAction.NONE -> {
927-
// Do nothing
928-
}
958+
GestureAction.SERVER_PREVIOUS -> presenter.previousServer()
959+
960+
GestureAction.OPEN_APP_SETTINGS -> startActivity(SettingsActivity.newInstance(this@WebViewActivity))
961+
962+
GestureAction.OPEN_APP_DEVELOPER -> startActivity(
963+
SettingsActivity.newInstance(
964+
context = this@WebViewActivity,
965+
screen = SettingsActivity.Deeplink.DEVELOPER,
966+
),
967+
)
929968
}
930969
}
931970
}
@@ -1686,6 +1725,26 @@ class WebViewActivity :
16861725
webView.evaluateJavascript(jsCode, null)
16871726
}
16881727

1728+
/**
1729+
* Send a key event to the webview's document root, to trigger frontend actions. Unlike the default
1730+
* [WebView.dispatchKeyEvent] function, this does not used the focused element (to avoid text inputs).
1731+
* The parameters should provide a JavaScript KeyboardEvent's properties.
1732+
*/
1733+
private fun WebView.dispatchKeyDownEventToDocument(key: String, code: String, keyCode: Int) {
1734+
val eventCode = """
1735+
var event = new KeyboardEvent('keydown', {
1736+
key: '$key',
1737+
code: '$code',
1738+
keyCode: $keyCode,
1739+
which: $keyCode,
1740+
bubbles: true,
1741+
cancelable: true
1742+
});
1743+
document.dispatchEvent(event);
1744+
""".trimIndent()
1745+
evaluateJavascript(eventCode, null)
1746+
}
1747+
16891748
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
16901749
// Workaround to sideload on Android TV and use a remote for basic navigation in WebView
16911750
if (event.keyCode == KeyEvent.KEYCODE_DPAD_DOWN && event.action == KeyEvent.ACTION_DOWN) {
@@ -1732,24 +1791,32 @@ class WebViewActivity :
17321791
if (presenter.isAlwaysShowFirstViewOnAppStartEnabled() &&
17331792
LifecycleHandler.isAppInBackground()
17341793
) {
1735-
// Clearing history and replace the current page with the default page from the frontend.
1736-
// This way the user have a clear history stack.
1737-
webView.clearHistory()
1738-
17391794
// Pattern matches urls which are NOT allowed to show the first view after app is started
17401795
// This is
17411796
// /config/* as these are the settings of HA but NOT /config/dashboard. This is just the overview of the HA settings
17421797
// /hassio/* as these are the addons section of HA settings.
17431798
if (webView.url?.matches(".*://.*/(config/(?!\\bdashboard\\b)|hassio)/*.*".toRegex()) == false) {
1744-
lifecycleScope.launch {
1745-
Timber.d("Show first view of default dashboard.")
1746-
if (serverManager.getServer(presenter.getActiveServer())?.version?.isAtLeast(2025, 6, 0) == true) {
1747-
sendExternalBusMessage(
1748-
NavigateTo("/", true),
1749-
)
1750-
} else {
1751-
webView.evaluateJavascript(
1752-
"""
1799+
Timber.d("Show first view of default dashboard.")
1800+
navigateToDefaultDashboard()
1801+
} else {
1802+
Timber.d("User is in the Home Assistant config. Will not show first view of the default dashboard.")
1803+
}
1804+
}
1805+
}
1806+
1807+
/** Clear history and replace the current page with the default dashboard. */
1808+
private fun navigateToDefaultDashboard() {
1809+
// This way the user have a clear history stack.
1810+
webView.clearHistory()
1811+
1812+
lifecycleScope.launch {
1813+
if (serverManager.getServer(presenter.getActiveServer())?.version?.isAtLeast(2025, 6, 0) == true) {
1814+
sendExternalBusMessage(
1815+
NavigateTo("/", true),
1816+
)
1817+
} else {
1818+
webView.evaluateJavascript(
1819+
"""
17531820
var anchor = 'a:nth-child(1)';
17541821
var defaultPanel = window.localStorage.getItem('defaultPanel')?.replaceAll('"',"");
17551822
if(defaultPanel) anchor = 'a[href="/' + defaultPanel + '"]';
@@ -1758,12 +1825,8 @@ class WebViewActivity :
17581825
.shadowRoot.querySelector('paper-listbox > ' + anchor).click();
17591826
window.scrollTo(0, 0);
17601827
""",
1761-
null,
1762-
)
1763-
}
1764-
}
1765-
} else {
1766-
Timber.d("User is in the Home Assistant config. Will not show first view of the default dashboard.")
1828+
null,
1829+
)
17671830
}
17681831
}
17691832
}

0 commit comments

Comments
 (0)