Skip to content

Commit

Permalink
Wear os authentication (#1691)
Browse files Browse the repository at this point in the history
* Initial proof-of-concept: sharing Session over data layer

* Add initial onboarding and login flow

In onboarding, the home assistant urls are received from connected devices. If the user clicks on it, the authentication flow starts.
The user can alter the login details and proceed to login.
The authentication uses the "auth/login_flow" api, instead of the normal authentication api, since there is no support for webview on wear os.

* Clean up wear and app communication

Clean up

* Add proof of authentication on home. And add logout button on home

* Update onboarding list

* Add loading views and error messages

* Move startup logic to HomeActivity to hopefully save some resources

* Add manual setup option and improve UI

* Cleanup

* Passing ktLintCheck

* Passing ktLintCheck after rebase

* Fix building after build.gradle changes during rebase

* Process review comments

Remove multiple product flavors
Remove unnecessary log
Replace margin with additional header
  • Loading branch information
leroyboerefijn authored Oct 1, 2021
1 parent d5ae97e commit 5f904b9
Show file tree
Hide file tree
Showing 65 changed files with 1,853 additions and 88 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ google-services.json
.idea/
.gradle/
build/
*.keystore
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:2.3.5")
implementation("com.google.android.material:material:1.4.0")

implementation("com.google.android.gms:play-services-wearable:17.1.0")

implementation("androidx.room:room-runtime:2.3.0")
implementation("androidx.room:room-ktx:2.3.0")
kapt("androidx.room:room-compiler:2.3.0")
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,15 @@
<activity
android:name=".onboarding.OnboardingActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />

<service android:name=".onboarding.WearOnboardingListener">
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<data android:scheme="wear" android:host="*"
android:path="/request_home_assistant_instance" />
</intent-filter>
</service>

<activity
android:name=".webview.WebViewActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.homeassistant.companion.android.onboarding

import dagger.Component
import io.homeassistant.companion.android.common.dagger.AppComponent

@Component(dependencies = [AppComponent::class])
interface OnboardingListenerComponent {

fun inject(listener: WearOnboardingListener)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.homeassistant.companion.android.onboarding

import android.util.Log
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.PutDataRequest
import com.google.android.gms.wearable.Wearable
import com.google.android.gms.wearable.WearableListenerService
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import kotlinx.coroutines.runBlocking
import javax.inject.Inject

class WearOnboardingListener : WearableListenerService() {

@Inject
lateinit var authenticationUseCase: AuthenticationRepository

@Inject
lateinit var urlUseCase: UrlRepository

override fun onCreate() {
super.onCreate()
DaggerOnboardingListenerComponent.builder()
.appComponent((applicationContext.applicationContext as GraphComponentAccessor).appComponent)
.build()
.inject(this)
}

override fun onMessageReceived(event: MessageEvent) {
Log.d("WearOnboardingListener", "onMessageReceived: $event")

if (event.path == "/request_home_assistant_instance") {
val nodeId = event.sourceNodeId
sendHomeAssistantInstance(nodeId)
}
}

private fun sendHomeAssistantInstance(nodeId: String) = runBlocking {
Log.d("WearOnboardingListener", "sendHomeAssistantInstance: $nodeId")
// Retrieve current instance
val url = urlUseCase.getUrl()

// Put as DataMap in data layer
val putDataReq: PutDataRequest = PutDataMapRequest.create("/home_assistant_instance").run {
dataMap.putString("name", url?.host.toString())
dataMap.putString("url", url.toString())
setUrgent()
asPutDataRequest()
}
Wearable.getDataClient(this@WearOnboardingListener).putDataItem(putDataReq).apply {
addOnSuccessListener { Log.d("WearOnboardingListener", "sendHomeAssistantInstance: success") }
addOnFailureListener { Log.d("WearOnboardingListener", "sendHomeAssistantInstance: failed") }
}
}
}
3 changes: 1 addition & 2 deletions app/src/main/res/layout/fragment_mobile_app_integration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/deviceName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
android:layout_height="wrap_content" />

</com.google.android.material.textfield.TextInputLayout>

Expand Down
8 changes: 8 additions & 0 deletions app/src/main/res/values/wear.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@array/android_wear_capabilities">
<string-array name="android_wear_capabilities">
<item>request_authentication_token</item>
<item>request_home_assistant_instance</item>
</string-array>
</resources>
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.homeassistant.companion.android.common.data.authentication

import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowCreateEntry
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowInit
import java.net.URL

interface AuthenticationRepository {

suspend fun initiateLoginFlow(): LoginFlowInit

suspend fun loginAuthentication(flowId: String, username: String, password: String): LoginFlowCreateEntry

suspend fun registerAuthorizationCode(authorizationCode: String)

suspend fun retrieveExternalAuthentication(forceRefresh: Boolean): String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ 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.LoginFlowInit
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowRequest
import io.homeassistant.companion.android.common.data.url.UrlRepository
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URL
Expand All @@ -26,6 +30,27 @@ class AuthenticationRepositoryImpl @Inject constructor(
private const val PREF_BIOMETRIC_ENABLED = "biometric_enabled"
}

override suspend fun initiateLoginFlow(): LoginFlowInit {
return authenticationService.initializeLogin(
LoginFlowRequest(
AuthenticationService.CLIENT_ID,
AuthenticationService.AUTH_CALLBACK,
AuthenticationService.HANDLER
)
)
}

override suspend fun loginAuthentication(flowId: String, username: String, password: String): LoginFlowCreateEntry {
return authenticationService.authenticate(
AuthenticationService.AUTHENTICATE_BASE_PATH + flowId,
LoginFlowAuthentication(
AuthenticationService.CLIENT_ID,
username,
password
)
)
}

override suspend fun registerAuthorizationCode(authorizationCode: String) {
authenticationService.getToken(
AuthenticationService.GRANT_TYPE_CODE,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
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.LoginFlowCreateEntry
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowInit
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowRequest
import io.homeassistant.companion.android.common.data.authentication.impl.entities.Token
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
import retrofit2.http.Url

interface AuthenticationService {

Expand All @@ -13,6 +19,9 @@ 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
Expand All @@ -37,4 +46,10 @@ interface AuthenticationService {
@Field("token") refreshToken: String,
@Field("action") action: String
)

@POST("auth/login_flow")
suspend fun initializeLogin(@Body body: LoginFlowRequest): LoginFlowInit

@POST
suspend fun authenticate(@Url url: String, @Body body: LoginFlowAuthentication): LoginFlowCreateEntry
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities

import com.fasterxml.jackson.annotation.JsonProperty

data class LoginFlowCreateEntry(
@JsonProperty("version")
val version: Int,
@JsonProperty("type")
val type: String,
@JsonProperty("flow_id")
val flowId: String,
@JsonProperty("result")
val result: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.homeassistant.companion.android.common.data.authentication.impl.entities

import com.fasterxml.jackson.annotation.JsonProperty

data class LoginFlowInit(
@JsonProperty("type")
val type: String,
@JsonProperty("flow_id")
val flowId: String,
@JsonProperty("step_id")
val stepId: String,
@JsonProperty("errors")
val errors: Map<String, String>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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?>
)
7 changes: 7 additions & 0 deletions wear/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ android {
signingConfig = signingConfigs.getByName("release")
}
}

kotlinOptions {
jvmTarget = "11"
}
Expand All @@ -65,9 +66,15 @@ android {
dependencies {
implementation(project(":common"))

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1")

implementation("com.google.android.material:material:1.4.0")

implementation("androidx.wear:wear:1.1.0")
implementation("com.google.android.support:wearable:2.8.1")
implementation("com.google.android.gms:play-services-wearable:17.1.0")
compileOnly("com.google.android.wearable:wearable:2.8.1")

implementation("com.google.dagger:dagger:2.38.1")
kapt("com.google.dagger:dagger-compiler:2.38.1")
}
21 changes: 13 additions & 8 deletions wear/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.homeassistant.companion.android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<uses-feature android:name="android.hardware.type.watch" />

<application
android:name=".HomeAssistantApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
Expand All @@ -17,23 +19,26 @@
android:name="com.google.android.wearable"
android:required="true" />

<!--
Set to true if your app is Standalone, that is, it does not require the handheld
app to run.
-->
<!-- The app can run without a connected phone -->
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />

<activity
android:name=".Home"
android:label="@string/app_name">
<activity android:name=".home.HomeActivity"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</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" />

<!-- To show confirmations and failures -->
<activity android:name="androidx.wear.activity.ConfirmationActivity" />
</application>

</manifest>
15 changes: 0 additions & 15 deletions wear/src/main/java/io/homeassistant/companion/android/Home.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.homeassistant.companion.android

import android.app.Application
import io.homeassistant.companion.android.common.dagger.AppComponent
import io.homeassistant.companion.android.common.dagger.Graph
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor

open class HomeAssistantApplication : Application(), GraphComponentAccessor {

lateinit var graph: Graph

override fun onCreate() {
super.onCreate()

graph = Graph(this, 0)
}

override val appComponent: AppComponent
get() = graph.appComponent
}
Loading

0 comments on commit 5f904b9

Please sign in to comment.