diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerChooserFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerChooserFragment.kt new file mode 100644 index 000000000..ae07ba295 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerChooserFragment.kt @@ -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(R.id.design_bottom_sheet) as FrameLayout + val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerChooserView.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerChooserView.kt new file mode 100644 index 000000000..a10366e53 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerChooserView.kt @@ -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, + 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)) +} diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt index bdf3c321d..d4fefbc86 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt @@ -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("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("activate_server_hint")?.let { + it.isVisible = true + it.onPreferenceClickListener = activateClickListener } } diff --git a/app/src/main/java/io/homeassistant/companion/android/util/compose/ModalBottomSheet.kt b/app/src/main/java/io/homeassistant/companion/android/util/compose/ModalBottomSheet.kt new file mode 100644 index 000000000..fd788a242 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/util/compose/ModalBottomSheet.kt @@ -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() + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt index f2dbb018f..538a94d13 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt @@ -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) { diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt index 206e4d4cf..f357d2e97 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenter.kt @@ -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) diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt index 2c7a3a416..266b96483 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewPresenterImpl.kt @@ -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 { diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml index 0128d40d3..12da3125d 100644 --- a/app/src/main/res/values-v27/styles.xml +++ b/app/src/main/res/values-v27/styles.xml @@ -14,4 +14,9 @@ @bool/isLightMode @bool/isLightMode + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 31dda3796..1893366bc 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -2,4 +2,5 @@ 16dp 8dp + 28dp \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3fdef8bef..effd4a9dd 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,6 +18,7 @@ @color/colorPrimaryDark @style/Theme.HomeAssistant.Dialog.Alert @style/ThemeOverlay.MaterialComponents.Dark + @style/ThemeOverlay.HomeAssistant.BottomSheetDialog + +