Update Wear onboarding to always use phone for sign in (#2838)

* Always do Wear onboarding on phone

 - Switch the Wear onboarding to always use the sign in flow on the paired phone, instead of offering the option to sign in completely on the watch.
 - When opening the companion app using the Wear app, immediately link the user to the list of instances or sign in screen.
 - Use standard OAuth flow for devices without companion app installed.
 - Fix type casting of location preference in Wear onboarding result.

* Fix ListenableFuture await import
This commit is contained in:
Joris Pelgröm 2022-09-05 16:25:13 +02:00 committed by GitHub
parent dab3640c6b
commit a711c659a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 336 additions and 599 deletions

View file

@ -173,12 +173,12 @@ class SettingsWearActivity : AppCompatActivity(), CapabilityClient.OnCapabilityC
}
wearNodesWithApp.size < allConnectedNodes.size -> {
Log.d(TAG, "Installed on some devices")
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp))
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp, getAuthIntentUrl()))
finish()
}
else -> {
Log.d(TAG, "Installed on all devices")
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp))
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp, getAuthIntentUrl()))
finish()
}
}
@ -226,6 +226,17 @@ class SettingsWearActivity : AppCompatActivity(), CapabilityClient.OnCapabilityC
}
}
private fun getAuthIntentUrl(): String? {
return intent.data?.let {
if (it.scheme == "homeassistant" && it.host == "wear-phone-signin") {
// Return empty string if phone sign in was used to open this, indicating no instance selected
it.getQueryParameter("url") ?: ""
} else {
null
}
}
}
companion object {
private const val TAG = "SettingsWearAct"

View file

@ -23,6 +23,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
@ -50,10 +52,10 @@ class SettingsWearViewModel @Inject constructor(
private val objectMapper = jacksonObjectMapper()
var hasData = mutableStateOf(false)
private set
var isAuthenticated = mutableStateOf(false)
private set
private val _hasData = MutableStateFlow(false)
val hasData = _hasData.asStateFlow()
private val _isAuthenticated = MutableStateFlow(false)
val isAuthenticated = _isAuthenticated.asStateFlow()
var entities = mutableStateMapOf<String, Entity<*>>()
private set
var supportedDomains = mutableStateListOf<String>()
@ -221,7 +223,7 @@ class SettingsWearViewModel @Inject constructor(
}
private fun onLoadConfigFromWear(data: DataMap) {
isAuthenticated.value = data.getBoolean(KEY_IS_AUTHENTICATED, false)
_isAuthenticated.value = data.getBoolean(KEY_IS_AUTHENTICATED, false)
val supportedDomainsList: List<String> =
objectMapper.readValue(data.getString(KEY_SUPPORTED_DOMAINS, "[\"input_boolean\", \"light\", \"lock\", \"switch\", \"script\", \"scene\"]"))
supportedDomains.clear()
@ -234,6 +236,6 @@ class SettingsWearViewModel @Inject constructor(
}
setTemplateContent(data.getString(KEY_TEMPLATE_TILE, ""))
templateTileRefreshInterval.value = data.getInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
hasData.value = true
_hasData.value = true
}
}

View file

@ -5,6 +5,8 @@ import android.net.Uri
import androidx.compose.material.IconButton
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
@ -37,10 +39,13 @@ fun LoadSettingsHomeView(
)
}
composable(SettingsWearMainView.LANDING) {
val hasData by settingsWearViewModel.hasData.collectAsState()
val isAuthenticated by settingsWearViewModel.isAuthenticated.collectAsState()
SettingWearLandingView(
deviceName = deviceName,
hasData = settingsWearViewModel.hasData.value,
isAuthed = settingsWearViewModel.isAuthenticated.value,
hasData = hasData,
isAuthed = isAuthenticated,
navigateFavorites = { navController.navigate(SettingsWearMainView.FAVORITES) },
navigateTemplateTile = { navController.navigate(SettingsWearMainView.TEMPLATE) },
loginWearOs = loginWearOs,

View file

@ -7,11 +7,14 @@ import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.wearable.Node
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.onboarding.OnboardApp
import io.homeassistant.companion.android.settings.wear.SettingsWearViewModel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@ -30,12 +33,14 @@ class SettingsWearMainView : AppCompatActivity() {
companion object {
private const val TAG = "SettingsWearDevice"
private var currentNodes = setOf<Node>()
private var registerUrl: String? = null
const val LANDING = "Landing"
const val FAVORITES = "Favorites"
const val TEMPLATE = "Template"
fun newInstance(context: Context, wearNodes: Set<Node>): Intent {
fun newInstance(context: Context, wearNodes: Set<Node>, url: String?): Intent {
currentNodes = wearNodes
registerUrl = url
return Intent(context, SettingsWearMainView::class.java)
}
}
@ -51,11 +56,23 @@ class SettingsWearMainView : AppCompatActivity() {
this::onBackPressed
)
}
if (registerUrl != null) {
lifecycleScope.launch {
settingsWearViewModel.hasData.collect { hasData ->
if (hasData) {
if (!settingsWearViewModel.isAuthenticated.value) loginWearOs()
this@launch.cancel() // Stop listening, we only need initial load
}
}
}
}
}
private fun loginWearOs() {
registerActivityResult.launch(
OnboardApp.Input(
url = registerUrl,
defaultDeviceName = currentNodes.firstOrNull()?.displayName ?: "unknown",
locationTrackingPossible = false
)

View file

@ -10,16 +10,19 @@ import io.homeassistant.companion.android.BuildConfig
class OnboardApp : ActivityResultContract<OnboardApp.Input, OnboardApp.Output?>() {
companion object {
private const val EXTRA_URL = "extra_url"
private const val EXTRA_DEFAULT_DEVICE_NAME = "extra_default_device_name"
private const val EXTRA_LOCATION_TRACKING_POSSIBLE = "location_tracking_possible"
fun parseInput(intent: Intent): Input = Input(
url = intent.getStringExtra(EXTRA_URL),
defaultDeviceName = intent.getStringExtra(EXTRA_DEFAULT_DEVICE_NAME) ?: Build.MODEL,
locationTrackingPossible = intent.getBooleanExtra(EXTRA_LOCATION_TRACKING_POSSIBLE, false),
)
}
data class Input(
val url: String? = null,
val defaultDeviceName: String = Build.MODEL,
val locationTrackingPossible: Boolean = BuildConfig.FLAVOR == "full"
)
@ -42,6 +45,7 @@ class OnboardApp : ActivityResultContract<OnboardApp.Input, OnboardApp.Output?>(
override fun createIntent(context: Context, input: Input): Intent {
return Intent(context, OnboardingActivity::class.java).apply {
putExtra(EXTRA_URL, input.url)
putExtra(EXTRA_DEFAULT_DEVICE_NAME, input.defaultDeviceName)
putExtra(EXTRA_LOCATION_TRACKING_POSSIBLE, input.locationTrackingPossible)
}

View file

@ -1,11 +1,16 @@
package io.homeassistant.companion.android.onboarding
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationFragment
import io.homeassistant.companion.android.onboarding.discovery.DiscoveryFragment
import io.homeassistant.companion.android.onboarding.manual.ManualSetupFragment
import io.homeassistant.companion.android.onboarding.welcome.WelcomeFragment
@AndroidEntryPoint
@ -26,10 +31,32 @@ class OnboardingActivity : AppCompatActivity() {
viewModel.locationTrackingPossible.value = input.locationTrackingPossible
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.content, WelcomeFragment::class.java, null)
.commit()
supportFragmentManager.commit {
add(R.id.content, WelcomeFragment::class.java, null)
}
if (input.url != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
supportFragmentManager.commit {
replace(R.id.content, DiscoveryFragment::class.java, null)
addToBackStack(null)
}
}
if (input.url.isNotBlank() || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
viewModel.onManualUrlUpdated(input.url)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !viewModel.manualContinueEnabled) {
supportFragmentManager.commit {
replace(R.id.content, ManualSetupFragment::class.java, null)
addToBackStack(null)
}
}
if (viewModel.manualContinueEnabled) {
supportFragmentManager.commit {
replace(R.id.content, AuthenticationFragment::class.java, null)
addToBackStack(null)
}
}
}
}
}
}

View file

@ -1,17 +1,7 @@
package io.homeassistant.companion.android.common.data.authentication
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowForm
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowResponse
import java.net.URL
interface AuthenticationRepository {
suspend fun initiateLoginFlow(): LoginFlowForm
suspend fun loginAuthentication(flowId: String, username: String, password: String): LoginFlowResponse?
suspend fun loginCode(flowId: String, code: String): LoginFlowResponse?
suspend fun registerAuthorizationCode(authorizationCode: String)
suspend fun retrieveExternalAuthentication(forceRefresh: Boolean): String
@ -23,7 +13,7 @@ interface AuthenticationRepository {
suspend fun getSessionState(): SessionState
suspend fun buildAuthenticationUrl(callbackUrl: String): URL
suspend fun buildAuthenticationUrl(baseUrl: String, callbackUrl: String): String
suspend fun buildBearerToken(): String

View file

@ -1,26 +1,13 @@
package io.homeassistant.companion.android.common.data.authentication.impl
import android.util.Log
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.homeassistant.companion.android.common.data.LocalStorage
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.authentication.AuthorizationException
import io.homeassistant.companion.android.common.data.authentication.SessionState
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowAuthentication
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowCreateEntry
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowForm
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowMfaCode
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowRequest
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowResponse
import io.homeassistant.companion.android.common.data.url.UrlRepository
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.ResponseBody
import java.net.URL
import javax.inject.Inject
import javax.inject.Named
@ -40,59 +27,6 @@ class AuthenticationRepositoryImpl @Inject constructor(
private const val PREF_BIOMETRIC_HOME_BYPASS_ENABLED = "biometric_home_bypass_enabled"
}
private val mapper = jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
override suspend fun initiateLoginFlow(): LoginFlowForm {
val url = urlRepository.getUrl()?.toHttpUrlOrNull().toString()
return authenticationService.initializeLogin(
url + "auth/login_flow",
LoginFlowRequest(
AuthenticationService.CLIENT_ID,
AuthenticationService.AUTH_CALLBACK,
AuthenticationService.HANDLER
)
)
}
override suspend fun loginAuthentication(flowId: String, username: String, password: String): LoginFlowResponse? {
val url = urlRepository.getUrl()?.toHttpUrlOrNull()
if (url == null) {
Log.e(TAG, "Unable to authenticate with username/password.")
throw AuthorizationException()
}
val response = authenticationService.authenticatePassword(
url.newBuilder().addPathSegments(AuthenticationService.AUTHENTICATE_BASE_PATH + flowId).build(),
LoginFlowAuthentication(
AuthenticationService.CLIENT_ID,
username,
password
)
)
if (!response.isSuccessful || response.body() == null) throw AuthorizationException()
return mapLoginFlowResponse(response.body()!!)
}
override suspend fun loginCode(flowId: String, code: String): LoginFlowResponse? {
val url = urlRepository.getUrl()?.toHttpUrlOrNull()
if (url == null) {
Log.e(TAG, "Unable to authenticate with MFA code.")
throw AuthorizationException()
}
val response = authenticationService.authenticateMfa(
url.newBuilder().addPathSegments(AuthenticationService.AUTHENTICATE_BASE_PATH + flowId).build(),
LoginFlowMfaCode(
AuthenticationService.CLIENT_ID,
code
)
)
if (!response.isSuccessful || response.body() == null) throw AuthorizationException()
return mapLoginFlowResponse(response.body()!!)
}
override suspend fun registerAuthorizationCode(authorizationCode: String) {
val url = urlRepository.getUrl()?.toHttpUrlOrNull()
if (url == null) {
@ -154,33 +88,21 @@ class AuthenticationRepositoryImpl @Inject constructor(
}
}
override suspend fun buildAuthenticationUrl(callbackUrl: String): URL {
val url = urlRepository.getUrl()
return url!!.toHttpUrlOrNull()!!
override suspend fun buildAuthenticationUrl(baseUrl: String, callbackUrl: String): String {
return baseUrl.toHttpUrlOrNull()!!
.newBuilder()
.addPathSegments("auth/authorize")
.addEncodedQueryParameter("response_type", "code")
.addEncodedQueryParameter("client_id", AuthenticationService.CLIENT_ID)
.addEncodedQueryParameter("redirect_uri", callbackUrl)
.build()
.toUrl()
.toString()
}
override suspend fun buildBearerToken(): String {
return "Bearer " + ensureValidSession().accessToken
}
private fun mapLoginFlowResponse(responseBody: ResponseBody): LoginFlowResponse? {
val responseText = responseBody.charStream().readText()
val message: JsonNode? = mapper.readValue(responseText)
return when (message?.get("type")?.textValue()) {
"form" -> mapper.readValue(responseText, LoginFlowForm::class.java)
"create_entry" -> mapper.readValue(responseText, LoginFlowCreateEntry::class.java)
else -> null
}
}
private fun convertSession(session: Session): String {
return ObjectMapper().writeValueAsString(
mapOf(

View file

@ -1,14 +1,8 @@
package io.homeassistant.companion.android.common.data.authentication.impl
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowAuthentication
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowForm
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowMfaCode
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowRequest
import io.homeassistant.companion.android.common.data.authentication.impl.entities.Token
import okhttp3.HttpUrl
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
@ -21,9 +15,6 @@ interface AuthenticationService {
const val GRANT_TYPE_CODE = "authorization_code"
const val GRANT_TYPE_REFRESH = "refresh_token"
const val REVOKE_ACTION = "revoke"
val HANDLER = listOf("homeassistant", null)
const val AUTHENTICATE_BASE_PATH = "auth/login_flow/"
const val AUTH_CALLBACK = "homeassistant://auth-callback"
}
@FormUrlEncoded
@ -51,13 +42,4 @@ interface AuthenticationService {
@Field("token") refreshToken: String,
@Field("action") action: String
)
@POST
suspend fun initializeLogin(@Url url: String, @Body body: LoginFlowRequest): LoginFlowForm
@POST
suspend fun authenticatePassword(@Url url: HttpUrl, @Body body: LoginFlowAuthentication): Response<ResponseBody>
@POST
suspend fun authenticateMfa(@Url url: HttpUrl, @Body body: LoginFlowMfaCode): Response<ResponseBody>
}

View file

@ -1,12 +0,0 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities
import com.fasterxml.jackson.annotation.JsonProperty
data class LoginFlowAuthentication(
@JsonProperty("client_id")
val clientId: String,
@JsonProperty("username")
val userName: String,
@JsonProperty("password")
val password: String
)

View file

@ -1,14 +0,0 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities
import com.fasterxml.jackson.annotation.JsonProperty
data class LoginFlowCreateEntry(
@JsonProperty("type")
override val type: String,
@JsonProperty("flow_id")
override val flowId: String,
@JsonProperty("version")
val version: Int,
@JsonProperty("result")
val result: String
) : LoginFlowResponse()

View file

@ -1,14 +0,0 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities
import com.fasterxml.jackson.annotation.JsonProperty
data class LoginFlowForm(
@JsonProperty("type")
override val type: String,
@JsonProperty("flow_id")
override val flowId: String,
@JsonProperty("step_id")
val stepId: String,
@JsonProperty("errors")
val errors: Map<String, String>
) : LoginFlowResponse()

View file

@ -1,10 +0,0 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities
import com.fasterxml.jackson.annotation.JsonProperty
data class LoginFlowMfaCode(
@JsonProperty("client_id")
val clientId: String,
@JsonProperty("code")
val code: String
)

View file

@ -1,12 +0,0 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities
import com.fasterxml.jackson.annotation.JsonProperty
data class LoginFlowRequest(
@JsonProperty("client_id")
val clientId: String,
@JsonProperty("redirect_uri")
val redirectUri: String,
@JsonProperty("handler")
val handler: List<String?>
)

View file

@ -1,6 +0,0 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities
sealed class LoginFlowResponse {
abstract val type: String
abstract val flowId: String
}

View file

@ -213,7 +213,9 @@
<string name="expand">Expand</string>
<string name="failed_authentication">Could not authenticate</string>
<string name="failed_connection">Could not connect</string>
<string name="failed_phone_connection">Could not connect to phone</string>
<string name="failed_registration">Could not register</string>
<string name="failed_unsupported">Not supported</string>
<string name="failed_scan">Scanning for Home Assistant failed.</string>
<string name="failure_send_favorites_wear">Failed to send favorites selection to Wear OS</string>
<string name="fans">Fans</string>
@ -342,8 +344,6 @@
<string name="message_missing_all">The Wear app is missing on your watch, click the button below to install the app.\n\nNote: Currently the Wear OS app requires you to be enrolled in the beta for the phone app. If the button does not work then please join the beta: https://play.google.com/apps/testing/io.homeassistant.companion.android</string>
<string name="message_no_connected_nodes">No connected Wear devices, please make sure Bluetooth is on and your watch is paired.</string>
<string name="message_some_installed">The Wear app is installed on some of your Wear devices: (%1$s)\n\nClick the button below to install the app on the other devices.\n\nNote: Currently the Wear OS app requires you to be enrolled in the beta for the phone app. If the button does not work then please join the beta: https://play.google.com/apps/testing/io.homeassistant.companion.android</string>
<string name="mfa_title">Two-factor\nAuthentication</string>
<string name="mfa_hint">Code</string>
<string name="missing_command_permission">Please open the Home Assistant app and send the command again in order to grant the proper permissions. You will be taken to a page to either grant the Home Assistant app the permission, or you will need to select Permissions from the details page and then grant the missing permission. For command_bluetooth the name of the permission is Nearby devices. If you are attempting to use command_activity to make a phone call you will also need to grant Phone permissions.</string>
<string name="areas">Areas</string>
<string name="more_entities">More entities</string>

View file

@ -98,6 +98,7 @@ dependencies {
implementation("com.google.android.gms:play-services-wearable:17.1.0")
implementation("androidx.wear:wear-input:1.2.0-alpha02")
implementation("androidx.wear:wear-remote-interactions:1.0.0")
implementation("androidx.wear:wear-phone-interactions:1.0.1")
compileOnly("com.google.android.wearable:wearable:2.9.0")
implementation("com.google.dagger:hilt-android:2.43.2")

View file

@ -52,7 +52,6 @@
</activity>
<activity android:name=".onboarding.OnboardingActivity" />
<activity android:name=".onboarding.integration.MobileAppIntegrationActivity" />
<activity android:name=".onboarding.authentication.AuthenticationActivity" />
<activity android:name=".onboarding.manual_setup.ManualSetupActivity" />
<activity android:name=".complications.ComplicationConfigActivity"
android:exported="true">

View file

@ -6,7 +6,9 @@ import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.wear.activity.ConfirmationActivity
import androidx.wear.remote.interactions.RemoteActivityHelper
@ -18,9 +20,11 @@ import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.Wearable
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationActivity
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity
import io.homeassistant.companion.android.onboarding.manual_setup.ManualSetupActivity
import io.homeassistant.companion.android.util.LoadingView
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
@ -44,6 +48,8 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
lateinit var presenter: OnboardingPresenter
private lateinit var loadingView: LoadingView
private var phoneSignInAvailable = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -52,9 +58,14 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
loadingView = findViewById(R.id.loading_view)
adapter = ServerListAdapter(ArrayList())
adapter.onInstanceClicked = { instance -> presenter.onAdapterItemClick(instance) }
adapter.onManualSetupClicked = { this.startManualSetup() }
adapter.onPhoneSignInClicked = { this.startPhoneSignIn() }
adapter.onInstanceClicked = { instance ->
if (phoneSignInAvailable) startPhoneSignIn(instance)
else presenter.onInstanceClickedWithoutApp(this, instance.url.toString())
}
adapter.onManualSetupClicked = {
if (phoneSignInAvailable) startPhoneSignIn(null)
else startManualSetup()
}
capabilityClient = Wearable.getCapabilityClient(this)
remoteActivityHelper = RemoteActivityHelper(this)
@ -90,45 +101,66 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
Wearable.getDataClient(this).removeListener(presenter)
}
override fun startAuthentication(flowId: String) {
startActivity(AuthenticationActivity.newInstance(this, flowId))
}
override fun startManualSetup() {
private fun startManualSetup() {
startActivity(ManualSetupActivity.newInstance(this))
}
override fun startPhoneSignIn() {
try {
remoteActivityHelper.startRemoteActivity(
Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_DEFAULT)
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse("homeassistant://wear-phone-signin")
},
null // a Wear device only has one companion device so this is not needed
)
val confirmation = Intent(this, ConfirmationActivity::class.java).apply {
putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.OPEN_ON_PHONE_ANIMATION)
putExtra(ConfirmationActivity.EXTRA_ANIMATION_DURATION_MILLIS, 2500)
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(commonR.string.continue_on_phone))
private fun startPhoneSignIn(instance: HomeAssistantInstance?) {
lifecycleScope.launch {
showLoading()
try {
val url = "homeassistant://wear-phone-signin${if (instance != null) "?url=${instance.url}" else ""}"
remoteActivityHelper.startRemoteActivity(
Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_DEFAULT)
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(url)
},
null // a Wear device only has one companion device so this is not needed
).await()
showContinueOnPhone()
} catch (e: Exception) {
if (e is RemoteActivityHelper.RemoteIntentException) {
Log.e(TAG, "Unable to open sign in activity on phone with app, falling back to OAuth", e)
if (instance != null) {
presenter.onInstanceClickedWithoutApp(this@OnboardingActivity, instance.url.toString())
} else {
startManualSetup()
}
} else {
Log.e(TAG, "Unable to open sign in activity on phone", e)
showError()
}
}
startActivity(confirmation)
} catch (e: Exception) {
Log.e(TAG, "Unable to open sign in activity on phone", e)
showError()
}
}
override fun startIntegration() {
startActivity(MobileAppIntegrationActivity.newInstance(this))
}
override fun showLoading() {
loadingView.visibility = View.VISIBLE
}
override fun showError() {
override fun showContinueOnPhone() {
val confirmation = Intent(this, ConfirmationActivity::class.java).apply {
putExtra(
ConfirmationActivity.EXTRA_ANIMATION_TYPE,
ConfirmationActivity.OPEN_ON_PHONE_ANIMATION
)
putExtra(ConfirmationActivity.EXTRA_ANIMATION_DURATION_MILLIS, 2000)
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(commonR.string.continue_on_phone))
}
startActivity(confirmation)
loadingView.visibility = View.GONE
}
override fun showError(@StringRes message: Int?) {
// Show failure message
val intent = Intent(this, ConfirmationActivity::class.java).apply {
putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.FAILURE_ANIMATION)
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(commonR.string.failed_connection))
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(message ?: commonR.string.failed_connection))
}
startActivity(intent)
loadingView.visibility = View.GONE
@ -202,10 +234,7 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
)
Log.d(TAG, "requestPhoneSignIn: found ${capabilityInfo.nodes.size} nodes")
runOnUiThread {
adapter.phoneSignInAvailable = capabilityInfo.nodes.size > 0
adapter.notifyDataSetChanged()
}
phoneSignInAvailable = capabilityInfo.nodes.size > 0
}
override fun onDestroy() {

View file

@ -1,11 +1,12 @@
package io.homeassistant.companion.android.onboarding
import android.content.Context
import com.google.android.gms.wearable.DataClient
import com.google.android.gms.wearable.DataMap
interface OnboardingPresenter : DataClient.OnDataChangedListener {
fun onAdapterItemClick(instance: HomeAssistantInstance)
fun onInstanceClickedWithoutApp(context: Context, url: String)
fun onFinish()

View file

@ -1,14 +1,20 @@
package io.homeassistant.companion.android.onboarding
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.wear.phone.interactions.authentication.CodeChallenge
import androidx.wear.phone.interactions.authentication.CodeVerifier
import androidx.wear.phone.interactions.authentication.OAuthRequest
import androidx.wear.phone.interactions.authentication.OAuthResponse
import androidx.wear.phone.interactions.authentication.RemoteAuthClient
import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.DataMap
import com.google.android.gms.wearable.DataMapItem
import dagger.hilt.android.qualifiers.ActivityContext
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowForm
import io.homeassistant.companion.android.common.data.url.UrlRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -16,7 +22,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.net.URL
import java.util.concurrent.Executors
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
class OnboardingPresenterImpl @Inject constructor(
@ActivityContext context: Context,
@ -28,9 +36,63 @@ class OnboardingPresenterImpl @Inject constructor(
}
private val view = context as OnboardingView
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
private var codeVerifier = CodeVerifier()
private var authClient: RemoteAuthClient? = null
override fun onInstanceClickedWithoutApp(context: Context, url: String) {
// This is very unlikely to happen, as instances are usually only discovered by the app.
// Still, the connection might be lost which is why this exists. Based on onNextClicked in
// ManualSetupPresenterImpl. Also a good starting point for manual URL if it is possible to
// enter this on the device without the app in the future.
mainScope.launch {
val request = OAuthRequest.Builder(context)
.setAuthProviderUrl(
Uri.parse(
authenticationUseCase.buildAuthenticationUrl(
url,
OAuthRequest.WEAR_REDIRECT_URL_PREFIX + BuildConfig.APPLICATION_ID
)
)
)
.setCodeChallenge(CodeChallenge(codeVerifier))
.build()
authClient = RemoteAuthClient.create(context)
authClient?.let {
view.showContinueOnPhone()
it.sendAuthorizationRequest(
request,
Executors.newSingleThreadExecutor(),
object : RemoteAuthClient.Callback() {
override fun onAuthorizationError(request: OAuthRequest, errorCode: Int) {
Log.w(TAG, "Received authorization error for OAuth: $errorCode")
view.showError(
when (errorCode) {
RemoteAuthClient.ERROR_UNSUPPORTED -> commonR.string.failed_unsupported
RemoteAuthClient.ERROR_PHONE_UNAVAILABLE -> commonR.string.failed_phone_connection
else -> commonR.string.failed_connection
}
)
}
override fun onAuthorizationResponse(
request: OAuthRequest,
response: OAuthResponse
) {
response.responseUrl?.getQueryParameter("code")?.let { code ->
register(url, code)
} ?: run {
view.showError(commonR.string.failed_registration)
}
}
}
)
}
}
}
override fun onDataChanged(dataEvents: DataEventBuffer) {
Log.d(TAG, "onDataChanged: [${dataEvents.count}]")
dataEvents.forEach { event ->
@ -64,27 +126,25 @@ class OnboardingPresenterImpl @Inject constructor(
}
}
override fun onAdapterItemClick(instance: HomeAssistantInstance) {
Log.d(TAG, "onAdapterItemClick: ${instance.name}")
view.showLoading()
fun register(url: String, code: String) {
mainScope.launch {
// Set current url
urlUseCase.saveUrl(instance.url.toString())
view.showLoading()
// Initiate login flow
try {
val flowForm: LoginFlowForm = authenticationUseCase.initiateLoginFlow()
Log.d(TAG, "Created login flow step ${flowForm.stepId}: ${flowForm.flowId}")
view.startAuthentication(flowForm.flowId)
urlUseCase.saveUrl(url)
authenticationUseCase.registerAuthorizationCode(code)
} catch (e: Exception) {
Log.e(TAG, "Unable to initiate login flow", e)
view.showError()
Log.e(TAG, "Exception during registration", e)
view.showError(commonR.string.failed_registration)
return@launch
}
view.startIntegration()
}
}
override fun onFinish() {
mainScope.cancel()
authClient?.close()
}
}

View file

@ -1,14 +1,16 @@
package io.homeassistant.companion.android.onboarding
import androidx.annotation.StringRes
interface OnboardingView {
fun startAuthentication(flowId: String)
fun startManualSetup()
fun startPhoneSignIn()
fun startIntegration()
fun onInstanceFound(instance: HomeAssistantInstance)
fun onInstanceLost(instance: HomeAssistantInstance)
fun showLoading()
fun showError()
fun showContinueOnPhone()
fun showError(@StringRes message: Int? = null)
}

View file

@ -9,7 +9,6 @@ import io.homeassistant.companion.android.viewHolders.HeaderViewHolder
import io.homeassistant.companion.android.viewHolders.InstanceViewHolder
import io.homeassistant.companion.android.viewHolders.LoadingViewHolder
import io.homeassistant.companion.android.viewHolders.ManualSetupViewHolder
import io.homeassistant.companion.android.viewHolders.PhoneSignInViewHolder
import kotlin.math.min
import io.homeassistant.companion.android.common.R as commonR
@ -19,16 +18,12 @@ class ServerListAdapter(
lateinit var onInstanceClicked: (HomeAssistantInstance) -> Unit
lateinit var onManualSetupClicked: () -> Unit
lateinit var onPhoneSignInClicked: () -> Unit
var phoneSignInAvailable = false
companion object {
private const val TYPE_INSTANCE = 1
private const val TYPE_HEADER = 2
private const val TYPE_LOADING = 3
private const val TYPE_MANUAL = 4
private const val TYPE_PHONE_SIGNIN = 5
}
override fun onCreateViewHolder(
@ -51,11 +46,6 @@ class ServerListAdapter(
.inflate(R.layout.listitem_instance, parent, false)
ManualSetupViewHolder(view, onManualSetupClicked)
}
TYPE_PHONE_SIGNIN -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.listitem_instance, parent, false)
PhoneSignInViewHolder(view, onPhoneSignInClicked)
}
else -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.listitem_loading, parent, false)
@ -69,8 +59,6 @@ class ServerListAdapter(
holder.server = servers[position - 1]
} else if (holder is ManualSetupViewHolder) {
holder.text.setText(commonR.string.manual_setup)
} else if (holder is PhoneSignInViewHolder) {
holder.text.setText(commonR.string.sign_in_on_phone)
} else if (holder is HeaderViewHolder) {
if (position == 0) {
holder.headerTextView.setText(commonR.string.list_header_instances)
@ -81,14 +69,13 @@ class ServerListAdapter(
}
override fun getItemCount() = min(
servers.size + (if (phoneSignInAvailable) 4 else 3),
if (phoneSignInAvailable) 5 else 4
servers.size + 3,
4
)
override fun getItemViewType(position: Int): Int {
return when {
position == 0 || position == this.itemCount - (if (phoneSignInAvailable) 3 else 2) -> TYPE_HEADER
position == this.itemCount - 2 && phoneSignInAvailable -> TYPE_PHONE_SIGNIN
position == 0 || position == this.itemCount - 2 -> TYPE_HEADER
position == this.itemCount - 1 -> TYPE_MANUAL
servers.size > 0 -> TYPE_INSTANCE
else -> TYPE_LOADING

View file

@ -1,91 +0,0 @@
package io.homeassistant.companion.android.onboarding.authentication
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.wear.activity.ConfirmationActivity
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.databinding.ActivityAuthenticationBinding
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
class AuthenticationActivity : AppCompatActivity(), AuthenticationView {
companion object {
private const val TAG = "AuthenticationActivity"
fun newInstance(context: Context, flowId: String): Intent {
var intent = Intent(context, AuthenticationActivity::class.java)
intent.putExtra("flowId", flowId)
return intent
}
}
@Inject
lateinit var presenter: AuthenticationPresenter
private lateinit var binding: ActivityAuthenticationBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent == null || !intent.hasExtra("flowId")) {
Log.e(TAG, "Flow id not specified, canceling authentication")
finish()
}
binding = ActivityAuthenticationBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.buttonNext.setOnClickListener {
presenter.onNextClicked(
intent.getStringExtra("flowId")!!,
binding.username.text.toString(),
binding.password.text.toString(),
binding.code.text.toString()
)
}
}
override fun onResume() {
super.onResume()
binding.loadingView.visibility = View.GONE
}
override fun startIntegration() {
startActivity(MobileAppIntegrationActivity.newInstance(this))
}
override fun showMfa() {
binding.titleLogin.visibility = View.GONE
binding.titleMfa.visibility = View.VISIBLE
binding.username.visibility = View.GONE
binding.password.visibility = View.GONE
binding.code.visibility = View.VISIBLE
binding.loadingView.visibility = View.GONE
}
override fun showLoading() {
binding.loadingView.visibility = View.VISIBLE
}
override fun showError() {
// Show failure message
val intent = Intent(this, ConfirmationActivity::class.java).apply {
putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.FAILURE_ANIMATION)
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(commonR.string.failed_authentication))
}
startActivity(intent)
binding.loadingView.visibility = View.GONE
}
override fun onDestroy() {
presenter.onFinish()
super.onDestroy()
}
}

View file

@ -1,14 +0,0 @@
package io.homeassistant.companion.android.onboarding.authentication
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
@Module
@InstallIn(ActivityComponent::class)
interface AuthenticationModule {
@Binds
fun authenticationPresenter(authenticationPresenterImpl: AuthenticationPresenterImpl): AuthenticationPresenter
}

View file

@ -1,8 +0,0 @@
package io.homeassistant.companion.android.onboarding.authentication
interface AuthenticationPresenter {
fun onNextClicked(flowId: String, username: String?, password: String?, code: String?)
fun onFinish()
}

View file

@ -1,74 +0,0 @@
package io.homeassistant.companion.android.onboarding.authentication
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ActivityContext
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowCreateEntry
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowForm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
class AuthenticationPresenterImpl @Inject constructor(
@ActivityContext context: Context,
private val authenticationUseCase: AuthenticationRepository
) : AuthenticationPresenter {
companion object {
private const val TAG = "AuthenticationPresenter"
}
enum class AuthenticationPhase {
PASSWORD, MFA
}
private var authenticationPhase = AuthenticationPhase.PASSWORD
private val view = context as AuthenticationView
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
override fun onNextClicked(flowId: String, username: String?, password: String?, code: String?) {
view.showLoading()
Log.d(TAG, "onNextClicked")
mainScope.launch {
try {
val flowResult = when (authenticationPhase) {
AuthenticationPhase.PASSWORD -> authenticationUseCase.loginAuthentication(flowId, username.orEmpty(), password.orEmpty())
AuthenticationPhase.MFA -> authenticationUseCase.loginCode(flowId, code.orEmpty())
}
Log.d(TAG, "Authenticated result type: ${flowResult?.type}")
when (flowResult?.type) {
"form" -> {
if (authenticationPhase == AuthenticationPhase.PASSWORD && (flowResult as LoginFlowForm).stepId == "mfa") {
Log.d(TAG, "MFA required to authenticate")
authenticationPhase = AuthenticationPhase.MFA
view.showMfa()
} else {
Log.e(TAG, "Unable to authenticate")
view.showError()
}
}
"create_entry" -> {
authenticationUseCase.registerAuthorizationCode((flowResult as LoginFlowCreateEntry).result)
Log.d(TAG, "Finished!")
view.startIntegration()
}
else -> {
Log.e(TAG, "Unable to authenticate")
view.showError()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate", e)
view.showError()
}
}
}
override fun onFinish() {
mainScope.cancel()
}
}

View file

@ -1,11 +0,0 @@
package io.homeassistant.companion.android.onboarding.authentication
interface AuthenticationView {
fun startIntegration()
fun showMfa()
fun showLoading()
fun showError()
}

View file

@ -5,12 +5,13 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.EditText
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.wear.activity.ConfirmationActivity
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.databinding.ActivityManualSetupBinding
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationActivity
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
@ -35,26 +36,39 @@ class ManualSetupActivity : AppCompatActivity(), ManualSetupView {
setContentView(binding.root)
binding.buttonNext.setOnClickListener {
presenter.onNextClicked(findViewById<EditText>(R.id.server_url).text.toString())
presenter.onNextClicked(this, findViewById<EditText>(R.id.server_url).text.toString())
}
}
override fun startAuthentication(flowId: String) {
startActivity(AuthenticationActivity.newInstance(this, flowId))
override fun startIntegration() {
startActivity(MobileAppIntegrationActivity.newInstance(this))
}
override fun showLoading() {
binding.loadingView.visibility = View.VISIBLE
}
override fun showError() {
override fun showContinueOnPhone() {
val confirmation = Intent(this, ConfirmationActivity::class.java).apply {
putExtra(
ConfirmationActivity.EXTRA_ANIMATION_TYPE,
ConfirmationActivity.OPEN_ON_PHONE_ANIMATION
)
putExtra(ConfirmationActivity.EXTRA_ANIMATION_DURATION_MILLIS, 2000)
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(commonR.string.continue_on_phone))
}
startActivity(confirmation)
binding.loadingView.visibility = View.GONE
}
override fun showError(@StringRes message: Int) {
// Show failure message
val intent = Intent(this, ConfirmationActivity::class.java).apply {
putExtra(
ConfirmationActivity.EXTRA_ANIMATION_TYPE,
ConfirmationActivity.FAILURE_ANIMATION
)
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(commonR.string.failed_connection))
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(message))
}
startActivity(intent)
binding.loadingView.visibility = View.GONE

View file

@ -1,8 +1,10 @@
package io.homeassistant.companion.android.onboarding.manual_setup
import android.content.Context
interface ManualSetupPresenter {
fun onNextClicked(url: String)
fun onNextClicked(context: Context, url: String)
fun onFinish()
}

View file

@ -1,17 +1,25 @@
package io.homeassistant.companion.android.onboarding.manual_setup
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.wear.phone.interactions.authentication.CodeChallenge
import androidx.wear.phone.interactions.authentication.CodeVerifier
import androidx.wear.phone.interactions.authentication.OAuthRequest
import androidx.wear.phone.interactions.authentication.OAuthResponse
import androidx.wear.phone.interactions.authentication.RemoteAuthClient
import dagger.hilt.android.qualifiers.ActivityContext
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowForm
import io.homeassistant.companion.android.common.data.url.UrlRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
class ManualSetupPresenterImpl @Inject constructor(
@ActivityContext context: Context,
@ -25,26 +33,77 @@ class ManualSetupPresenterImpl @Inject constructor(
private val view = context as ManualSetupView
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
override fun onNextClicked(url: String) {
private var codeVerifier = CodeVerifier()
private var authClient: RemoteAuthClient? = null
override fun onNextClicked(context: Context, url: String) {
view.showLoading()
mainScope.launch {
// Set current url
urlUseCase.saveUrl(url)
val request = OAuthRequest.Builder(context)
.setAuthProviderUrl(
Uri.parse(
authenticationUseCase.buildAuthenticationUrl(
url,
OAuthRequest.WEAR_REDIRECT_URL_PREFIX + BuildConfig.APPLICATION_ID
)
)
)
.setCodeChallenge(CodeChallenge(codeVerifier))
.build()
// Initiate login flow
try {
val flowForm: LoginFlowForm = authenticationUseCase.initiateLoginFlow()
Log.d(TAG, "Created login flow step ${flowForm.stepId}: ${flowForm.flowId}")
authClient = RemoteAuthClient.create(context)
authClient?.let {
view.showContinueOnPhone()
it.sendAuthorizationRequest(
request,
Executors.newSingleThreadExecutor(),
object : RemoteAuthClient.Callback() {
override fun onAuthorizationError(request: OAuthRequest, errorCode: Int) {
Log.w(TAG, "Received authorization error for OAuth: $errorCode")
view.showError(
when (errorCode) {
RemoteAuthClient.ERROR_UNSUPPORTED -> commonR.string.failed_unsupported
RemoteAuthClient.ERROR_PHONE_UNAVAILABLE -> commonR.string.failed_phone_connection
else -> commonR.string.failed_connection
}
)
}
view.startAuthentication(flowForm.flowId)
} catch (e: Exception) {
Log.e(TAG, "Unable to initiate login flow", e)
view.showError()
override fun onAuthorizationResponse(
request: OAuthRequest,
response: OAuthResponse
) {
response.responseUrl?.getQueryParameter("code")?.let { code ->
register(url, code)
} ?: run {
view.showError(commonR.string.failed_registration)
}
}
}
)
}
}
}
fun register(url: String, code: String) {
mainScope.launch {
view.showLoading()
try {
urlUseCase.saveUrl(url)
authenticationUseCase.registerAuthorizationCode(code)
} catch (e: Exception) {
Log.e(TAG, "Exception during registration", e)
view.showError(commonR.string.failed_registration)
return@launch
}
view.startIntegration()
}
}
override fun onFinish() {
mainScope.cancel()
authClient?.close()
}
}

View file

@ -1,9 +1,13 @@
package io.homeassistant.companion.android.onboarding.manual_setup
import androidx.annotation.StringRes
interface ManualSetupView {
fun startAuthentication(flowId: String)
fun startIntegration()
fun showLoading()
fun showError()
fun showContinueOnPhone()
fun showError(@StringRes message: Int)
}

View file

@ -113,7 +113,7 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
val url = dataMap.getString("URL")
val authCode = dataMap.getString("AuthCode")
val deviceName = dataMap.getString("DeviceName")
val deviceTrackingEnabled = dataMap.getString("LocationTracking")
val deviceTrackingEnabled = dataMap.getBoolean("LocationTracking")
urlRepository.saveUrl(url)
authenticationRepository.registerAuthorizationCode(authCode)

View file

@ -1,19 +0,0 @@
package io.homeassistant.companion.android.viewHolders
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.homeassistant.companion.android.R
class PhoneSignInViewHolder(v: View, val onClick: () -> Unit) :
RecyclerView.ViewHolder(v) {
val text: TextView = v.findViewById(R.id.txt_name)
init {
// Set onclick listener
v.setOnClickListener {
onClick()
}
}
}

View file

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.wear.widget.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/box_inset_layout_padding"
tools:context=".onboarding.authentication.AuthenticationActivity"
tools:deviceIds="wear">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/inner_frame_layout_padding"
app:layout_boxedEdges="all">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/button_forward"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_button_arrow_forward" />
<TextView
android:id="@+id/titleLogin"
style="@style/HeaderText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/titleMfa"
style="@style/HeaderText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mfa_title"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"/>
<EditText
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/username"
android:importantForAutofill="no"
android:inputType="textPersonName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleLogin" />
<EditText
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/password"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username"
android:imeOptions="actionDone"
android:importantForAutofill="no" />
<EditText
android:id="@+id/code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/mfa_hint"
android:inputType="number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleMfa"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<io.homeassistant.companion.android.util.LoadingView
android:id="@+id/loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading_text="@string/attempting_authentication"
android:visibility="gone" />
</androidx.wear.widget.BoxInsetLayout>