Server gestures and chooser (#3346)

* Server gestures and chooser

 - Add gestures to quickly switch between multiple servers
 - Add a chooser to select a server, and add a gesture for it

* Fix history (new page is added _after_ loading finishes)
This commit is contained in:
Joris Pelgröm 2023-02-23 20:04:25 +01:00 committed by GitHub
parent 7412dd22ab
commit 4bda27387c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 265 additions and 14 deletions

View file

@ -0,0 +1,62 @@
package io.homeassistant.companion.android.settings.server
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.ui.platform.ComposeView
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import com.google.accompanist.themeadapter.material.MdcTheme
import com.google.android.material.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.servers.ServerManager
import javax.inject.Inject
@AndroidEntryPoint
class ServerChooserFragment : BottomSheetDialogFragment() {
@Inject
lateinit var serverManager: ServerManager
companion object {
const val TAG = "ServerChooser"
const val RESULT_KEY = "ServerChooserResult"
const val RESULT_SERVER = "server"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
MdcTheme {
ServerChooserView(
servers = serverManager.defaultServers,
onServerSelected = { serverId ->
setFragmentResult(RESULT_KEY, bundleOf(RESULT_SERVER to serverId))
dismiss()
}
)
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialog?.setOnShowListener {
val dialog = it as BottomSheetDialog
val bottomSheet = dialog.findViewById<View>(R.id.design_bottom_sheet) as FrameLayout
val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}

View file

@ -0,0 +1,52 @@
package io.homeassistant.companion.android.settings.server
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.homeassistant.companion.android.database.server.Server
import io.homeassistant.companion.android.util.compose.ModalBottomSheet
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun ServerChooserView(
servers: List<Server>,
onServerSelected: (Int) -> Unit
) {
ModalBottomSheet(title = stringResource(commonR.string.server_select)) {
servers.forEach {
ServerChooserRow(server = it, onServerSelected = onServerSelected)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
fun ServerChooserRow(
server: Server,
onServerSelected: (Int) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.dp)
.clickable { onServerSelected(server.id) }
) {
Text(
text = server.friendlyName,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
Divider(modifier = Modifier.padding(horizontal = 16.dp))
}

View file

@ -18,6 +18,7 @@ import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceClickListener
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
@ -81,15 +82,20 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() {
}
if (presenter.hasMultipleServers()) {
val activateClickListener = OnPreferenceClickListener {
val intent = WebViewActivity.newInstance(requireContext(), null, serverId).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
requireContext().startActivity(intent)
return@OnPreferenceClickListener true
}
findPreference<Preference>("activate_server")?.let {
it.isVisible = true
it.setOnPreferenceClickListener {
val intent = WebViewActivity.newInstance(requireContext(), null, serverId).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
requireContext().startActivity(intent)
return@setOnPreferenceClickListener true
}
it.onPreferenceClickListener = activateClickListener
}
findPreference<Preference>("activate_server_hint")?.let {
it.isVisible = true
it.onPreferenceClickListener = activateClickListener
}
}

View file

@ -0,0 +1,66 @@
package io.homeassistant.companion.android.util.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.R as commonR
/**
* A Material 3-style modal bottom sheet with an optional handle, for use with a
* [com.google.android.material.bottomsheet.BottomSheetDialogFragment].
*/
@Composable
fun ModalBottomSheet(
title: String,
showHandle: Boolean = true,
content: @Composable () -> Unit
) {
val sheetCornerRadius = dimensionResource(R.dimen.bottom_sheet_corner_radius)
Surface(
shape = RoundedCornerShape(topStart = sheetCornerRadius, topEnd = sheetCornerRadius)
) {
Column {
if (showHandle) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 22.dp),
horizontalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.size(width = 24.dp, height = 4.dp)
.clip(RoundedCornerShape(2.dp))
.background(colorResource(commonR.color.colorBottomSheetHandle))
)
}
}
Text(
text = title,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 22.dp),
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center
)
content()
}
}
}

View file

@ -90,6 +90,7 @@ import io.homeassistant.companion.android.nfc.WriteNfcTag
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.sensors.SensorWorker
import io.homeassistant.companion.android.settings.SettingsActivity
import io.homeassistant.companion.android.settings.server.ServerChooserFragment
import io.homeassistant.companion.android.themes.ThemesManager
import io.homeassistant.companion.android.util.ChangeLog
import io.homeassistant.companion.android.util.DataUriDownloadManager
@ -211,6 +212,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
private var exoBottom: Int = 0
private var exoMute: Boolean = true
private var failedConnection = "external"
private var clearHistory = false
private var moreInfoEntity = ""
private val moreInfoMutex = Mutex()
private var currentAutoplay: Boolean = false
@ -278,11 +280,24 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
direction: SwipeDirection,
pointerCount: Int
): Boolean {
if (pointerCount == 3 &&
direction == SwipeDirection.DOWN &&
velocity >= 150
) {
dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_E))
if (pointerCount == 3 && velocity >= 75) {
when (direction) {
SwipeDirection.LEFT -> presenter.nextServer()
SwipeDirection.RIGHT -> presenter.previousServer()
SwipeDirection.UP -> {
val serverChooser = ServerChooserFragment()
supportFragmentManager.setFragmentResultListener(ServerChooserFragment.RESULT_KEY, this@WebViewActivity) { _, bundle ->
if (bundle.containsKey(ServerChooserFragment.RESULT_SERVER)) {
presenter.switchActiveServer(bundle.getInt(ServerChooserFragment.RESULT_SERVER))
}
supportFragmentManager.clearFragmentResultListener(ServerChooserFragment.RESULT_KEY)
}
serverChooser.show(supportFragmentManager, ServerChooserFragment.TAG)
}
SwipeDirection.DOWN -> {
dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_E))
}
}
}
return appLocked
}
@ -311,6 +326,10 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
}
override fun onPageFinished(view: WebView?, url: String?) {
if (clearHistory) {
webView.clearHistory()
clearHistory = false
}
enablePinchToZoom()
if (moreInfoEntity != "" && view?.progress == 100 && isConnected) {
ioScope.launch {
@ -1072,9 +1091,9 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
override fun loadUrl(url: String, keepHistory: Boolean) {
loadedUrl = url
clearHistory = !keepHistory
webView.loadUrl(url)
waitForConnection()
if (!keepHistory) webView.clearHistory()
}
override fun setStatusBarAndNavigationBarColor(statusBarColor: Int, navigationBarColor: Int) {

View file

@ -13,6 +13,9 @@ interface WebViewPresenter {
fun getActiveServer(): Int
fun updateActiveServer()
fun setActiveServer(id: Int)
fun switchActiveServer(id: Int)
fun nextServer()
fun previousServer()
fun onGetExternalAuth(context: Context, callback: String, force: Boolean)

View file

@ -130,6 +130,27 @@ class WebViewPresenterImpl @Inject constructor(
}
}
override fun switchActiveServer(id: Int) {
setActiveServer(id)
onViewReady(null)
}
override fun nextServer() = moveToServer(next = true)
override fun previousServer() = moveToServer(next = false)
private fun moveToServer(next: Boolean) {
val servers = serverManager.defaultServers
if (servers.size < 2) return
val currentServerIndex = servers.indexOfFirst { it.id == serverId }
if (currentServerIndex > -1) {
var newServerIndex = if (next) currentServerIndex + 1 else currentServerIndex - 1
if (newServerIndex == servers.size) newServerIndex = 0
if (newServerIndex < 0) newServerIndex = servers.size - 1
servers.getOrNull(newServerIndex)?.let { switchActiveServer(it.id) }
}
}
override fun checkSecurityVersion() {
mainScope.launch {
try {

View file

@ -14,4 +14,9 @@
<item name="android:windowLightStatusBar">@bool/isLightMode</item>
<item name="android:windowLightNavigationBar">@bool/isLightMode</item>
</style>
<style name="ThemeOverlay.HomeAssistant.BottomSheetDialog" parent="ThemeOverlay.HomeAssistant.BottomSheetDialog.Base">
<item name="android:navigationBarColor">@color/colorSurface</item>
<item name="android:windowLightNavigationBar">@bool/isLightMode</item>
</style>
</resources>

View file

@ -2,4 +2,5 @@
<resources>
<dimen name="activity_margin">16dp</dimen>
<dimen name="widget_corner_radius">8dp</dimen>
<dimen name="bottom_sheet_corner_radius">28dp</dimen>
</resources>

View file

@ -18,6 +18,7 @@
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
<item name="alertDialogTheme">@style/Theme.HomeAssistant.Dialog.Alert</item>
<item name="actionBarPopupTheme">@style/ThemeOverlay.MaterialComponents.Dark</item>
<item name="bottomSheetDialogTheme">@style/ThemeOverlay.HomeAssistant.BottomSheetDialog</item>
</style>
<style name="Theme.HomeAssistant.Config" parent="Theme.HomeAssistant" />
@ -55,6 +56,12 @@
<item name="popupTheme">@style/Theme.HomeAssistant.PopupTheme</item>
</style>
<style name="ThemeOverlay.HomeAssistant.BottomSheetDialog" parent="ThemeOverlay.HomeAssistant.BottomSheetDialog.Base"/>
<style name="ThemeOverlay.HomeAssistant.BottomSheetDialog.Base" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
<item name="android:backgroundTint">@android:color/transparent</item>
</style>
<style name="ThemeOverlay.HomeAssistant.ConfigActionBar" parent="ThemeOverlay.MaterialComponents.ActionBar">
<item name="android:background">@color/colorBackground</item>
<item name="android:textColorPrimary">@color/colorOnBackground</item>

View file

@ -6,6 +6,10 @@
android:icon="@drawable/ic_launch"
android:title="@string/server_activate"
app:isPreferenceVisible="false"/>
<Preference
android:key="activate_server_hint"
android:summary="@string/server_activate_swipe_hint"
app:isPreferenceVisible="false"/>
<PreferenceCategory
android:title="@string/device_registration">
<EditTextPreference

View file

@ -5,6 +5,7 @@
<color name="colorDialogTitle">@android:color/white</color>
<color name="colorBackground">#1c1c1c</color>
<color name="colorOnBackground">#e1e1e1</color>
<color name="colorSurface">#121212</color>
<color name="colorPrimary">#03A9F4</color>
<color name="colorPrimaryDark">#111111</color>
<color name="colorAccent">#03A9F4</color>
@ -22,4 +23,5 @@
<color name="colorActionBarPopupBackground">#2B2B2B</color>
<color name="colorLaunchScreenBackground">#111111</color>
<color name="colorCodeBackground">#282828</color>
<color name="colorBottomSheetHandle">#66CAC4D0</color> <!-- M3 On Surface Variant 40% opacity -->
</resources>

View file

@ -5,6 +5,7 @@
<color name="colorDialogTitle">@android:color/black</color>
<color name="colorBackground">@android:color/white</color>
<color name="colorOnBackground">#212121</color>
<color name="colorSurface">@android:color/white</color>
<color name="colorPrimary">#03A9F4</color>
<color name="colorPrimaryDark">#0288D1</color>
<color name="colorAccent">#03A9F4</color>
@ -33,4 +34,5 @@
<color name="colorDeviceControlsThermostatHeat">#FF8B66</color>
<color name="colorDeviceControlsCamera">#F1F3F4</color>
<color name="colorSpeechText">#B3E5FC</color>
<color name="colorBottomSheetHandle">#6649454E</color> <!-- M3 On Surface Variant 40% opacity -->
</resources>

View file

@ -651,7 +651,8 @@
<string name="sensors">Sensors</string>
<string name="server_settings">Server Settings</string>
<string name="servers_devices_category">Servers &amp; Devices</string>
<string name="server_activate">Activate Server</string>
<string name="server_activate">Activate</string>
<string name="server_activate_swipe_hint">Tip: Quickly activate using a three-finger swipe left, right, or up when viewing a server.</string>
<string name="server_add">Add Server</string>
<string name="server_add_success">Server added</string>
<string name="server_add_failed">Couldn\'t add server</string>