diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d50f45ddda2..edf3817edfe 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -118,6 +118,7 @@ android {
dependencies {
implementation(project(":common"))
+ implementation(libs.androidx.browser)
coreLibraryDesugaring(libs.tools.desugar.jdk)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 33b95e07742..e6c0aca8c0a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -306,7 +306,19 @@
+ android:theme="@style/Theme.HomeAssistant.Config"
+ android:exported="true">
+
+
+
+
+
+
+
+
+
()
@@ -66,15 +102,22 @@ class OnboardingActivity : BaseActivity() {
}
}
if (viewModel.manualContinueEnabled) {
- supportFragmentManager.commit {
- replace(R.id.content, AuthenticationFragment::class.java, null)
- addToBackStack(null)
- }
+ val uri = buildAuthUrl(baseContext, input.url)
+ val builder = CustomTabsIntent.Builder()
+ val customTabsIntent = builder.build()
+ customTabsIntent.launchUrl(baseContext, Uri.parse(uri))
}
}
}
}
+ if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
+ val uri = intent.data
+ if (uri != null && uri.scheme == "homeassistant") {
+ handleAuthCallback(uri.toString())
+ }
+ }
+
val onBackPressed = object : OnBackPressedCallback(supportFragmentManager.backStackEntryCount > 0) {
override fun handleOnBackPressed() {
supportFragmentManager.popBackStack()
@@ -86,6 +129,18 @@ class OnboardingActivity : BaseActivity() {
}
}
+ private fun handleAuthCallback(url: String) {
+ val code = Uri.parse(url).getQueryParameter("code")
+ if (url.startsWith(AUTH_CALLBACK) && !code.isNullOrBlank()) {
+ viewModel.registerAuthCode(code)
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.content, MobileAppIntegrationFragment::class.java, null)
+ .addToBackStack(null)
+ .commit()
+ }
+ }
+
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// Workaround to sideload on Android TV and use a remote for basic navigation in WebView
val fragmentManager = supportFragmentManager.findFragmentByTag(AUTHENTICATION_FRAGMENT)
diff --git a/app/src/main/java/io/homeassistant/companion/android/onboarding/authentication/AuthenticationFragment.kt b/app/src/main/java/io/homeassistant/companion/android/onboarding/authentication/AuthenticationFragment.kt
deleted file mode 100644
index 0260666c928..00000000000
--- a/app/src/main/java/io/homeassistant/companion/android/onboarding/authentication/AuthenticationFragment.kt
+++ /dev/null
@@ -1,249 +0,0 @@
-package io.homeassistant.companion.android.onboarding.authentication
-
-import android.annotation.SuppressLint
-import android.net.Uri
-import android.net.http.SslError
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.webkit.SslErrorHandler
-import android.webkit.WebResourceError
-import android.webkit.WebResourceRequest
-import android.webkit.WebResourceResponse
-import android.webkit.WebView
-import android.webkit.WebViewClient
-import android.widget.Toast
-import androidx.annotation.RequiresApi
-import androidx.appcompat.app.AlertDialog
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import dagger.hilt.android.AndroidEntryPoint
-import io.homeassistant.companion.android.R
-import io.homeassistant.companion.android.common.R as commonR
-import io.homeassistant.companion.android.common.data.HomeAssistantApis
-import io.homeassistant.companion.android.common.data.authentication.impl.AuthenticationService
-import io.homeassistant.companion.android.common.data.keychain.KeyChainRepository
-import io.homeassistant.companion.android.onboarding.OnboardingViewModel
-import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationFragment
-import io.homeassistant.companion.android.themes.ThemesManager
-import io.homeassistant.companion.android.util.TLSWebViewClient
-import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
-import io.homeassistant.companion.android.util.isStarted
-import javax.inject.Inject
-import javax.inject.Named
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrl
-
-@AndroidEntryPoint
-class AuthenticationFragment : Fragment() {
-
- companion object {
- private const val TAG = "AuthenticationFragment"
- private const val AUTH_CALLBACK = "homeassistant://auth-callback"
- }
-
- private val viewModel by activityViewModels()
-
- private var authUrl: String? = null
-
- @Inject
- lateinit var themesManager: ThemesManager
-
- @Inject
- @Named("keyChainRepository")
- lateinit var keyChainRepository: KeyChainRepository
-
- @SuppressLint("SetJavaScriptEnabled")
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- return ComposeView(requireContext()).apply {
- setContent {
- HomeAssistantAppTheme {
- AndroidView({
- WebView(requireContext()).apply {
- themesManager.setThemeForWebView(requireContext(), settings)
- settings.javaScriptEnabled = true
- settings.domStorageEnabled = true
- settings.userAgentString = settings.userAgentString + " ${HomeAssistantApis.USER_AGENT_STRING}"
- webViewClient = object : TLSWebViewClient(keyChainRepository) {
- @Deprecated("Deprecated in Java")
- override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
- return onRedirect(url)
- }
-
- @RequiresApi(Build.VERSION_CODES.M)
- override fun onReceivedError(
- view: WebView?,
- request: WebResourceRequest?,
- error: WebResourceError?
- ) {
- super.onReceivedError(view, request, error)
- if (request?.url?.toString() == authUrl) {
- Log.e(
- TAG,
- "onReceivedError: Status Code: ${error?.errorCode} Description: ${error?.description}"
- )
- showError(
- requireContext().getString(
- commonR.string.error_http_generic,
- error?.errorCode,
- if (error?.description.isNullOrBlank()) {
- commonR.string.no_description
- } else {
- error?.description
- }
- ),
- null,
- error
- )
- }
- }
-
- override fun onReceivedHttpError(
- view: WebView?,
- request: WebResourceRequest?,
- errorResponse: WebResourceResponse?
- ) {
- super.onReceivedHttpError(view, request, errorResponse)
- if (request?.url?.toString() == authUrl) {
- Log.e(
- TAG,
- "onReceivedHttpError: Status Code: ${errorResponse?.statusCode} Description: ${errorResponse?.reasonPhrase}"
- )
- if (isTLSClientAuthNeeded && !isCertificateChainValid) {
- showError(
- requireContext().getString(commonR.string.tls_cert_expired_message),
- null,
- null
- )
- } else if (isTLSClientAuthNeeded && errorResponse?.statusCode == 400) {
- showError(
- requireContext().getString(commonR.string.tls_cert_not_found_message),
- null,
- null
- )
- } else {
- showError(
- requireContext().getString(
- commonR.string.error_http_generic,
- errorResponse?.statusCode,
- if (errorResponse?.reasonPhrase.isNullOrBlank()) {
- requireContext().getString(commonR.string.no_description)
- } else {
- errorResponse?.reasonPhrase
- }
- ),
- null,
- null
- )
- }
- }
- }
-
- override fun onReceivedSslError(
- view: WebView?,
- handler: SslErrorHandler?,
- error: SslError?
- ) {
- super.onReceivedSslError(view, handler, error)
- Log.e(TAG, "onReceivedSslError: $error")
- showError(requireContext().getString(commonR.string.error_ssl), error, null)
- }
- }
- authUrl = buildAuthUrl(viewModel.manualUrl.value)
- loadUrl(authUrl!!)
- }
- })
- }
- }
- }
- }
-
- private fun buildAuthUrl(base: String): String {
- return try {
- val url = base.toHttpUrl()
- val builder = if (url.host.endsWith("ui.nabu.casa", true)) {
- HttpUrl.Builder()
- .scheme(url.scheme)
- .host(url.host)
- .port(url.port)
- } else {
- url.newBuilder()
- }
- builder
- .addPathSegments("auth/authorize")
- .addEncodedQueryParameter("response_type", "code")
- .addEncodedQueryParameter("client_id", AuthenticationService.CLIENT_ID)
- .addEncodedQueryParameter("redirect_uri", AUTH_CALLBACK)
- .build()
- .toString()
- } catch (e: Exception) {
- Log.e(TAG, "Unable to build authentication URL", e)
- Toast.makeText(context, commonR.string.error_connection_failed, Toast.LENGTH_LONG).show()
- parentFragmentManager.popBackStack()
- ""
- }
- }
-
- private fun onRedirect(url: String): Boolean {
- val code = Uri.parse(url).getQueryParameter("code")
- return if (url.startsWith(AUTH_CALLBACK) && !code.isNullOrBlank()) {
- viewModel.registerAuthCode(code)
- parentFragmentManager
- .beginTransaction()
- .replace(R.id.content, MobileAppIntegrationFragment::class.java, null)
- .addToBackStack(null)
- .commit()
- true
- } else {
- // The WebViewClient should load this URL
- authUrl = url
- false
- }
- }
-
- private fun showError(message: String, sslError: SslError?, error: WebResourceError?) {
- if (!isStarted) {
- // Fragment is at least paused, can't display alert
- return
- }
- AlertDialog.Builder(requireContext())
- .setTitle(commonR.string.error_connection_failed)
- .setMessage(
- when (sslError?.primaryError) {
- SslError.SSL_DATE_INVALID -> requireContext().getString(commonR.string.webview_error_SSL_DATE_INVALID)
- SslError.SSL_EXPIRED -> requireContext().getString(commonR.string.webview_error_SSL_EXPIRED)
- SslError.SSL_IDMISMATCH -> requireContext().getString(commonR.string.webview_error_SSL_IDMISMATCH)
- SslError.SSL_INVALID -> requireContext().getString(commonR.string.webview_error_SSL_INVALID)
- SslError.SSL_NOTYETVALID -> requireContext().getString(commonR.string.webview_error_SSL_NOTYETVALID)
- SslError.SSL_UNTRUSTED -> requireContext().getString(commonR.string.webview_error_SSL_UNTRUSTED)
- else -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- when (error?.errorCode) {
- WebViewClient.ERROR_FAILED_SSL_HANDSHAKE ->
- requireContext().getString(commonR.string.webview_error_FAILED_SSL_HANDSHAKE)
- WebViewClient.ERROR_AUTHENTICATION -> requireContext().getString(commonR.string.webview_error_AUTHENTICATION)
- WebViewClient.ERROR_PROXY_AUTHENTICATION -> requireContext().getString(commonR.string.webview_error_PROXY_AUTHENTICATION)
- WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME -> requireContext().getString(commonR.string.webview_error_AUTH_SCHEME)
- WebViewClient.ERROR_HOST_LOOKUP -> requireContext().getString(commonR.string.webview_error_HOST_LOOKUP)
- else -> message
- }
- } else {
- message
- }
- }
- }
- )
- .setPositiveButton(android.R.string.ok) { _, _ -> }
- .show()
- parentFragmentManager.popBackStack()
- }
-}
diff --git a/app/src/main/java/io/homeassistant/companion/android/onboarding/discovery/DiscoveryFragment.kt b/app/src/main/java/io/homeassistant/companion/android/onboarding/discovery/DiscoveryFragment.kt
index ebc327a81e2..ca7d218ab1d 100644
--- a/app/src/main/java/io/homeassistant/companion/android/onboarding/discovery/DiscoveryFragment.kt
+++ b/app/src/main/java/io/homeassistant/companion/android/onboarding/discovery/DiscoveryFragment.kt
@@ -1,9 +1,12 @@
package io.homeassistant.companion.android.onboarding.discovery
+import android.content.Context
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -12,8 +15,8 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
+import io.homeassistant.companion.android.onboarding.OnboardingActivity
import io.homeassistant.companion.android.onboarding.OnboardingViewModel
-import io.homeassistant.companion.android.onboarding.authentication.AuthenticationFragment
import io.homeassistant.companion.android.onboarding.manual.ManualSetupFragment
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
import javax.inject.Inject
@@ -47,7 +50,7 @@ class DiscoveryFragment @Inject constructor() : Fragment() {
discoveryActive = viewModel.discoveryActive,
foundInstances = viewModel.foundInstances,
manualSetupClicked = { navigateToManualSetup() },
- instanceClicked = { onInstanceClicked(it) }
+ instanceClicked = { onInstanceClicked(it, requireContext()) }
)
}
}
@@ -62,12 +65,11 @@ class DiscoveryFragment @Inject constructor() : Fragment() {
.commit()
}
- private fun onInstanceClicked(instance: HomeAssistantInstance) {
+ private fun onInstanceClicked(instance: HomeAssistantInstance, context: Context) {
viewModel.manualUrl.value = instance.url.toString()
- parentFragmentManager
- .beginTransaction()
- .replace(R.id.content, AuthenticationFragment::class.java, null)
- .addToBackStack(null)
- .commit()
+ val uri = OnboardingActivity.buildAuthUrl(context, viewModel.manualUrl.value)
+ val builder = CustomTabsIntent.Builder()
+ val customTabsIntent = builder.build()
+ customTabsIntent.launchUrl(context, Uri.parse(uri))
}
}
diff --git a/app/src/main/java/io/homeassistant/companion/android/onboarding/manual/ManualSetupFragment.kt b/app/src/main/java/io/homeassistant/companion/android/onboarding/manual/ManualSetupFragment.kt
index 0d7a7dee7d7..06ee6564711 100644
--- a/app/src/main/java/io/homeassistant/companion/android/onboarding/manual/ManualSetupFragment.kt
+++ b/app/src/main/java/io/homeassistant/companion/android/onboarding/manual/ManualSetupFragment.kt
@@ -1,16 +1,18 @@
package io.homeassistant.companion.android.onboarding.manual
+import android.content.Context
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
-import io.homeassistant.companion.android.R
+import io.homeassistant.companion.android.onboarding.OnboardingActivity
import io.homeassistant.companion.android.onboarding.OnboardingViewModel
-import io.homeassistant.companion.android.onboarding.authentication.AuthenticationFragment
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
@AndroidEntryPoint
@@ -30,18 +32,17 @@ class ManualSetupFragment : Fragment() {
manualUrl = viewModel.manualUrl,
onManualUrlUpdated = viewModel::onManualUrlUpdated,
manualContinueEnabled = viewModel.manualContinueEnabled,
- connectedClicked = { connectClicked() }
+ connectedClicked = { connectClicked(requireContext(), viewModel.manualUrl.value) }
)
}
}
}
}
- private fun connectClicked() {
- parentFragmentManager
- .beginTransaction()
- .replace(R.id.content, AuthenticationFragment::class.java, null)
- .addToBackStack(null)
- .commit()
+ private fun connectClicked(context: Context, url: String) {
+ val uri = OnboardingActivity.buildAuthUrl(context, url)
+ val builder = CustomTabsIntent.Builder()
+ val customTabsIntent = builder.build()
+ customTabsIntent.launchUrl(context, Uri.parse(uri))
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8486d438916..44f4ef9df9a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -68,6 +68,8 @@ webkit = "1.12.1"
wear-remote-interactions = "1.1.0"
workRuntimeKtx = "2.10.0"
zxing = "4.3.0"
+customTabs = "1.8.0"
+browser = "1.8.0"
[plugins]
android-application = { id = "com.android.application", version.ref = "androidPlugin" }
@@ -173,6 +175,8 @@ wear-tiles = { module = "androidx.wear.tiles:tiles", version.ref = "wear-tiles"
wear-tooling = { module = "androidx.wear:wear-tooling-preview", version.ref = "wear-tooling" }
webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
zxing = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" }
+customTabs = { group = "androidx.browser", name = "browser", version.ref = "customTabs" }
+androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
[bundles]
coil = ["coil-views", "coil-oktthp", "coil-svg"]