Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import io.homeassistant.companion.android.settings.notification.NotificationHist
import io.homeassistant.companion.android.settings.qs.ManageTilesFragment
import io.homeassistant.companion.android.settings.sensor.SensorDetailFragment
import io.homeassistant.companion.android.settings.server.ServerSettingsFragment
import io.homeassistant.companion.android.settings.ssid.SsidFragment
import io.homeassistant.companion.android.settings.websocket.WebsocketSettingFragment
import io.homeassistant.companion.android.util.applySafeDrawingInsets
import javax.inject.Inject
Expand Down Expand Up @@ -62,6 +63,7 @@ class SettingsActivity : BaseActivity() {
@Parcelize
sealed interface Deeplink : Parcelable {
data object Developer : Deeplink
data class HomeNetwork(val serverId: Int) : Deeplink
data object NotificationHistory : Deeplink
data class QSTile(val tileId: String) : Deeplink
data class Sensor(val sensorId: String) : Deeplink
Expand Down Expand Up @@ -100,12 +102,17 @@ class SettingsActivity : BaseActivity() {
SettingsFragment::class.java
}
Deeplink.Developer -> DeveloperSettingsFragment::class.java
is Deeplink.HomeNetwork -> SsidFragment::class.java
Deeplink.NotificationHistory -> NotificationHistoryFragment::class.java
is Deeplink.Sensor -> SensorDetailFragment::class.java
is Deeplink.QSTile -> ManageTilesFragment::class.java
else -> SettingsFragment::class.java
},
when (settingsNavigation) {
is Deeplink.HomeNetwork -> {
Bundle().apply { putInt(SsidFragment.EXTRA_SERVER, settingsNavigation.serverId) }
}

is Deeplink.Sensor -> {
SensorDetailFragment.newInstance(settingsNavigation.sensorId).arguments
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import io.homeassistant.companion.android.common.util.toJsonObject
import io.homeassistant.companion.android.common.util.toJsonObjectOrNull
import io.homeassistant.companion.android.database.authentication.Authentication
import io.homeassistant.companion.android.database.authentication.AuthenticationDao
import io.homeassistant.companion.android.database.server.SecurityStatus
import io.homeassistant.companion.android.databinding.DialogAuthenticationBinding
import io.homeassistant.companion.android.improv.ui.ImprovPermissionDialog
import io.homeassistant.companion.android.improv.ui.ImprovSetupDialog
Expand Down Expand Up @@ -131,6 +132,7 @@ import io.homeassistant.companion.android.webview.externalbus.ExternalConfigResp
import io.homeassistant.companion.android.webview.externalbus.ExternalEntityAddToAction
import io.homeassistant.companion.android.webview.externalbus.NavigateTo
import io.homeassistant.companion.android.webview.externalbus.ShowSidebar
import io.homeassistant.companion.android.webview.insecure.BlockInsecureFragment
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -1392,8 +1394,7 @@ class WebViewActivity :
clearHistory = !keepHistory
lifecycleScope.launch {
if (!presenter.shouldSetSecurityLevel()) {
webView.loadUrl(url)
waitForConnection()
secureLoadUrl(url)
} else {
val serverId = presenter.getActiveServer()
Timber.d("Security level not set for server $serverId, showing ConnectionSecurityLevelFragment")
Expand All @@ -1410,6 +1411,42 @@ class WebViewActivity :
}
}

/**
* Make sure we only load the url if the securityLevel and current states allow it.
*/
private suspend fun secureLoadUrl(url: String) {
fun loadAndWait() {
webView.loadUrl(url)
waitForConnection()
}

if (url.startsWith("https")) {
loadAndWait()
return
}

val allowInsecureConnection = presenter.getAllowInsecureConnection()

when (allowInsecureConnection) {
null, true -> loadAndWait()
false -> {
val status = serverManager.getServer(
presenter.getActiveServer(),
)?.connection?.currentSecurityStatusForUrl(this, url)
when (status) {
is SecurityStatus.Insecure, null -> {
showBlockInsecureFragment(
serverId = presenter.getActiveServer(),
missingHomeSetup = status?.missingHomeSetup ?: false,
missingLocation = status?.missingLocation ?: false,
)
}
SecurityStatus.Secure -> loadAndWait()
}
}
}
}

private fun showConnectionSecurityLevelFragment(serverId: Int) {
supportFragmentManager.setFragmentResultListener(
ConnectionSecurityLevelFragment.RESULT_KEY,
Expand All @@ -1419,8 +1456,9 @@ class WebViewActivity :
supportFragmentManager.clearFragmentResultListener(ConnectionSecurityLevelFragment.RESULT_KEY)

if (::loadedUrl.isInitialized) {
webView.loadUrl(loadedUrl)
waitForConnection()
lifecycleScope.launch {
secureLoadUrl(loadedUrl)
}
}
}

Expand All @@ -1430,6 +1468,33 @@ class WebViewActivity :
.commit()
}

private fun showBlockInsecureFragment(serverId: Int, missingHomeSetup: Boolean, missingLocation: Boolean) {
supportFragmentManager.setFragmentResultListener(
BlockInsecureFragment.RESULT_KEY,
this,
) { _, _ ->
Timber.d("Block insecure screen exited by user, retrying URL loading")
supportFragmentManager.clearFragmentResultListener(BlockInsecureFragment.RESULT_KEY)

if (::loadedUrl.isInitialized) {
lifecycleScope.launch {
secureLoadUrl(loadedUrl)
}
}
}
supportFragmentManager.beginTransaction()
.replace(
android.R.id.content,
BlockInsecureFragment.newInstance(
serverId = serverId,
missingHomeSetup = missingHomeSetup,
missingLocation = missingLocation,
),
)
.addToBackStack(null)
.commit()
}

override fun setStatusBarAndBackgroundColor(statusBarColor: Int, backgroundColor: Int) {
// Set background colors
if (statusBarColor != 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ interface WebViewPresenter {
*/
suspend fun shouldSetSecurityLevel(): Boolean

suspend fun getAllowInsecureConnection(): Boolean?

suspend fun getAuthorizationHeader(): String

suspend fun parseWebViewColor(webViewColor: String): Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ class WebViewPresenterImpl @Inject constructor(
return serverManager.integrationRepository(serverId).getAllowInsecureConnection() == null
}

override suspend fun getAllowInsecureConnection(): Boolean? =
serverManager.integrationRepository(serverId).getAllowInsecureConnection()

override suspend fun getAuthorizationHeader(): String {
return serverManager.getServer(serverId)?.let {
serverManager.authenticationRepository(serverId).buildBearerToken()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.homeassistant.companion.android.webview.insecure

import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalUriHandler
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.compose.theme.HATheme
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
import io.homeassistant.companion.android.settings.ConnectionSecurityLevelFragment
import io.homeassistant.companion.android.settings.SettingsActivity

/**
* Fragment explaining why the current connection is blocked.
*/
@AndroidEntryPoint
class BlockInsecureFragment private constructor() : Fragment() {

companion object {
const val RESULT_KEY = "block_insecure_result"
private const val EXTRA_SERVER = "server_id"
private const val EXTRA_MISSING_HOME_SETUP = "missing_home_setup"
private const val EXTRA_MISSING_LOCATION = "missing_location"

fun newInstance(serverId: Int, missingHomeSetup: Boolean, missingLocation: Boolean): BlockInsecureFragment {
return BlockInsecureFragment().apply {
arguments = Bundle().apply {
putInt(EXTRA_SERVER, serverId)
putBoolean(EXTRA_MISSING_HOME_SETUP, missingHomeSetup)
putBoolean(EXTRA_MISSING_LOCATION, missingLocation)
}
}
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val serverId = arguments?.getInt(EXTRA_SERVER, ServerManager.SERVER_ID_ACTIVE) ?: ServerManager.SERVER_ID_ACTIVE
val missingHomeSetup = arguments?.getBoolean(EXTRA_MISSING_HOME_SETUP, false) ?: false
val missingLocation = arguments?.getBoolean(EXTRA_MISSING_LOCATION, false) ?: false

return ComposeView(requireContext()).apply {
setContent {
val uriHandler = LocalUriHandler.current
HATheme {
BlockInsecureScreen(
missingHomeSetup = missingHomeSetup,
missingLocation = missingLocation,
onRetry = ::retryAndClose,
onHelpClick = {
uriHandler.openUri(
"https://companion.home-assistant.io/docs/getting_started/connection-security-level/",
)
},
onOpenSettings = {
startActivity(SettingsActivity.newInstance(requireContext()))
},
onChangeSecurityLevel = {
showConnectionSecurityLevelFragment(serverId)
},
onOpenLocationSettings = ::openLocationSettings,
onConfigureHomeNetwork = {
startActivity(
SettingsActivity.newInstance(
context = requireContext(),
screen = SettingsActivity.Deeplink.HomeNetwork(serverId),
),
)
},
)
}
}
}
}

private fun openLocationSettings() {
if (DisabledLocationHandler.isLocationEnabled(requireContext())) {
retryAndClose()
return
}
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
}
if (intent.resolveActivity(requireContext().packageManager) == null) {
intent.action = Settings.ACTION_SETTINGS
}
startActivity(intent)
}

private fun retryAndClose() {
setFragmentResult(RESULT_KEY, Bundle())
parentFragmentManager.popBackStack()
}

private fun showConnectionSecurityLevelFragment(serverId: Int) {
val fragment = ConnectionSecurityLevelFragment.newInstance(
serverId = serverId,
handleAllInsets = true,
)
parentFragmentManager.beginTransaction()
.replace(android.R.id.content, fragment)
.addToBackStack(null)
.commit()
}
}
Loading
Loading