mirror of
https://github.com/home-assistant/android
synced 2024-10-02 22:34:46 +00:00
Wear os authentication (#1691)
* 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
This commit is contained in:
parent
d5ae97ed50
commit
5f904b980e
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,3 +7,4 @@ google-services.json
|
|||
.idea/
|
||||
.gradle/
|
||||
build/
|
||||
*.keystore
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
8
app/src/main/res/values/wear.xml
Normal file
8
app/src/main/res/values/wear.xml
Normal file
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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>
|
||||
)
|
|
@ -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?>
|
||||
)
|
|
@ -53,6 +53,7 @@ android {
|
|||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -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>
|
|
@ -1,15 +0,0 @@
|
|||
package io.homeassistant.companion.android
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.wearable.activity.WearableActivity
|
||||
|
||||
class Home : WearableActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_home)
|
||||
|
||||
// Enables Always-on
|
||||
setAmbientEnabled()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.homeassistant.companion.android
|
||||
|
||||
import dagger.Component
|
||||
import io.homeassistant.companion.android.common.dagger.AppComponent
|
||||
import io.homeassistant.companion.android.home.HomeActivity
|
||||
import io.homeassistant.companion.android.onboarding.OnboardingActivity
|
||||
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
|
||||
|
||||
@Component(dependencies = [AppComponent::class], modules = [PresenterModule::class])
|
||||
interface PresenterComponent {
|
||||
|
||||
fun inject(activity: OnboardingActivity)
|
||||
|
||||
fun inject(activity: AuthenticationActivity)
|
||||
|
||||
fun inject(activity: MobileAppIntegrationActivity)
|
||||
|
||||
fun inject(activity: ManualSetupActivity)
|
||||
|
||||
fun inject(activity: HomeActivity)
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package io.homeassistant.companion.android
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.homeassistant.companion.android.home.HomePresenter
|
||||
import io.homeassistant.companion.android.home.HomePresenterImpl
|
||||
import io.homeassistant.companion.android.home.HomeView
|
||||
import io.homeassistant.companion.android.onboarding.OnboardingPresenter
|
||||
import io.homeassistant.companion.android.onboarding.OnboardingPresenterImpl
|
||||
import io.homeassistant.companion.android.onboarding.OnboardingView
|
||||
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationPresenter
|
||||
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationPresenterImpl
|
||||
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationView
|
||||
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationPresenter
|
||||
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationPresenterImpl
|
||||
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationView
|
||||
import io.homeassistant.companion.android.onboarding.manual_setup.ManualSetupPresenter
|
||||
import io.homeassistant.companion.android.onboarding.manual_setup.ManualSetupPresenterImpl
|
||||
import io.homeassistant.companion.android.onboarding.manual_setup.ManualSetupView
|
||||
|
||||
@Module(includes = [PresenterModule.Declaration::class])
|
||||
class PresenterModule {
|
||||
|
||||
private lateinit var onBoardingView: OnboardingView
|
||||
private lateinit var authenticationView: AuthenticationView
|
||||
private lateinit var mobileAppIntegrationView: MobileAppIntegrationView
|
||||
private lateinit var manualSetupView: ManualSetupView
|
||||
private lateinit var homeView: HomeView
|
||||
|
||||
constructor(onBoardingView: OnboardingView) {
|
||||
this.onBoardingView = onBoardingView
|
||||
}
|
||||
|
||||
constructor(authenticationView: AuthenticationView) {
|
||||
this.authenticationView = authenticationView
|
||||
}
|
||||
|
||||
constructor(mobileAppIntegrationView: MobileAppIntegrationView) {
|
||||
this.mobileAppIntegrationView = mobileAppIntegrationView
|
||||
}
|
||||
|
||||
constructor(manualSetupView: ManualSetupView) {
|
||||
this.manualSetupView = manualSetupView
|
||||
}
|
||||
|
||||
constructor(homeView: HomeView) {
|
||||
this.homeView = homeView
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideOnboardingView() = onBoardingView
|
||||
|
||||
@Provides
|
||||
fun provideAuthenticationView() = authenticationView
|
||||
|
||||
@Provides
|
||||
fun provideMobileAppIntegrationView() = mobileAppIntegrationView
|
||||
|
||||
@Provides
|
||||
fun provideManualSetupView() = manualSetupView
|
||||
|
||||
@Provides
|
||||
fun provideHomeView() = homeView
|
||||
|
||||
@Module
|
||||
interface Declaration {
|
||||
|
||||
@Binds
|
||||
fun bindOnboardingPresenter(presenter: OnboardingPresenterImpl): OnboardingPresenter
|
||||
|
||||
@Binds
|
||||
fun bindAuthenticationPresenter(presenter: AuthenticationPresenterImpl): AuthenticationPresenter
|
||||
|
||||
@Binds
|
||||
fun bindMobileAppIntegrationPresenter(presenter: MobileAppIntegrationPresenterImpl): MobileAppIntegrationPresenter
|
||||
|
||||
@Binds
|
||||
fun bindManualSetupPresenter(presenter: ManualSetupPresenterImpl): ManualSetupPresenter
|
||||
|
||||
@Binds
|
||||
fun bindHomePresenter(presenter: HomePresenterImpl): HomePresenter
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package io.homeassistant.companion.android.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import io.homeassistant.companion.android.DaggerPresenterComponent
|
||||
import io.homeassistant.companion.android.PresenterModule
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.onboarding.OnboardingActivity
|
||||
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeActivity : AppCompatActivity(), HomeView {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: HomePresenter
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HomeActivity"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, HomeActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_home)
|
||||
|
||||
DaggerPresenterComponent
|
||||
.builder()
|
||||
.appComponent((application as GraphComponentAccessor).appComponent)
|
||||
.presenterModule(PresenterModule(this))
|
||||
.build()
|
||||
.inject(this)
|
||||
|
||||
findViewById<MaterialButton>(R.id.btn_logout).setOnClickListener {
|
||||
presenter.onLogoutClicked()
|
||||
}
|
||||
|
||||
presenter.onViewReady()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.onFinish()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun showHomeAssistantVersion(version: String) {
|
||||
val txtVersion = findViewById<TextView>(R.id.txt_version)
|
||||
txtVersion.text = getString(R.string.version, version)
|
||||
}
|
||||
|
||||
override fun showEntitiesCount(count: Int) {
|
||||
val txtEntities = findViewById<TextView>(R.id.txt_entities)
|
||||
txtEntities.text = resources.getQuantityString(R.plurals.entities_found, count, count)
|
||||
}
|
||||
|
||||
override fun displayOnBoarding() {
|
||||
val intent = OnboardingActivity.newInstance(this)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun displayMobileAppIntegration() {
|
||||
val intent = MobileAppIntegrationActivity.newInstance(this)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.homeassistant.companion.android.home
|
||||
|
||||
interface HomePresenter {
|
||||
|
||||
fun onViewReady()
|
||||
fun onLogoutClicked()
|
||||
fun onFinish()
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package io.homeassistant.companion.android.home
|
||||
|
||||
import android.util.Log
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
|
||||
import io.homeassistant.companion.android.common.data.authentication.SessionState
|
||||
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
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 HomePresenterImpl @Inject constructor(
|
||||
private val view: HomeView,
|
||||
private val authenticationUseCase: AuthenticationRepository,
|
||||
private val integrationUseCase: IntegrationRepository
|
||||
) : HomePresenter {
|
||||
|
||||
companion object {
|
||||
const val TAG = "HomePresenter"
|
||||
}
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun onViewReady() {
|
||||
mainScope.launch {
|
||||
val sessionValid = authenticationUseCase.getSessionState() == SessionState.CONNECTED
|
||||
if (sessionValid && integrationUseCase.isRegistered()) {
|
||||
resyncRegistration()
|
||||
// We'll stay on HomeActivity, so start loading
|
||||
view.showHomeAssistantVersion(integrationUseCase.getHomeAssistantVersion())
|
||||
view.showEntitiesCount(integrationUseCase.getEntities().size)
|
||||
} else if (sessionValid) {
|
||||
view.displayMobileAppIntegration()
|
||||
} else {
|
||||
view.displayOnBoarding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLogoutClicked() {
|
||||
mainScope.launch {
|
||||
authenticationUseCase.revokeSession()
|
||||
view.displayOnBoarding()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
mainScope.cancel()
|
||||
}
|
||||
|
||||
private fun resyncRegistration() {
|
||||
mainScope.launch {
|
||||
try {
|
||||
integrationUseCase.updateRegistration(
|
||||
DeviceRegistration(
|
||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||
null,
|
||||
null
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Issue updating Registration", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.homeassistant.companion.android.home
|
||||
|
||||
interface HomeView {
|
||||
fun showHomeAssistantVersion(version: String)
|
||||
fun showEntitiesCount(count: Int)
|
||||
|
||||
fun displayOnBoarding()
|
||||
fun displayMobileAppIntegration()
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.homeassistant.companion.android.onboarding
|
||||
|
||||
import java.net.URL
|
||||
|
||||
data class HomeAssistantInstance(
|
||||
val name: String,
|
||||
val url: URL,
|
||||
val version: String
|
||||
)
|
|
@ -0,0 +1,172 @@
|
|||
package io.homeassistant.companion.android.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.wear.activity.ConfirmationActivity
|
||||
import androidx.wear.widget.WearableRecyclerView
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.android.gms.wearable.CapabilityClient
|
||||
import com.google.android.gms.wearable.CapabilityInfo
|
||||
import com.google.android.gms.wearable.DataMapItem
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import io.homeassistant.companion.android.DaggerPresenterComponent
|
||||
import io.homeassistant.companion.android.PresenterModule
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationActivity
|
||||
import io.homeassistant.companion.android.onboarding.manual_setup.ManualSetupActivity
|
||||
import io.homeassistant.companion.android.util.LoadingView
|
||||
import javax.inject.Inject
|
||||
|
||||
class OnboardingActivity : AppCompatActivity(), OnboardingView {
|
||||
|
||||
private lateinit var adapter: ServerListAdapter
|
||||
|
||||
companion object {
|
||||
private const val TAG = "OnboardingActivity"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, OnboardingActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: OnboardingPresenter
|
||||
private lateinit var loadingView: LoadingView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
DaggerPresenterComponent
|
||||
.builder()
|
||||
.appComponent((application as GraphComponentAccessor).appComponent)
|
||||
.presenterModule(PresenterModule(this))
|
||||
.build()
|
||||
.inject(this)
|
||||
|
||||
setContentView(R.layout.activity_onboarding)
|
||||
|
||||
loadingView = findViewById<LoadingView>(R.id.loading_view)
|
||||
|
||||
adapter = ServerListAdapter(ArrayList())
|
||||
adapter.onInstanceClicked = { instance -> presenter.onAdapterItemClick(instance) }
|
||||
adapter.onManualSetupClicked = { this.startManualSetup() }
|
||||
|
||||
findViewById<WearableRecyclerView>(R.id.server_list)?.apply {
|
||||
layoutManager = LinearLayoutManager(this@OnboardingActivity)
|
||||
isEdgeItemsCenteringEnabled = true
|
||||
adapter = this@OnboardingActivity.adapter
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
loadingView.visibility = View.GONE
|
||||
|
||||
// Add listener to exchange authentication tokens
|
||||
Wearable.getDataClient(this).addListener(presenter)
|
||||
|
||||
// Check for current instances
|
||||
Thread { findExistingInstances() }.start()
|
||||
|
||||
// Request authentication token in separate task
|
||||
Thread { requestInstances() }.start()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
Wearable.getDataClient(this).removeListener(presenter)
|
||||
}
|
||||
|
||||
override fun startAuthentication(flowId: String) {
|
||||
startActivity(AuthenticationActivity.newInstance(this, flowId))
|
||||
}
|
||||
|
||||
override fun startManualSetup() {
|
||||
startActivity(ManualSetupActivity.newInstance(this))
|
||||
}
|
||||
|
||||
override fun showLoading() {
|
||||
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(R.string.failed_connection))
|
||||
}
|
||||
startActivity(intent)
|
||||
loadingView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onInstanceFound(instance: HomeAssistantInstance) {
|
||||
Log.d(TAG, "onInstanceFound: ${instance.name}")
|
||||
if (!adapter.servers.contains(instance)) {
|
||||
adapter.servers.add(instance)
|
||||
adapter.notifyDataSetChanged()
|
||||
Log.d(TAG, "onInstanceFound: added ${instance.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInstanceLost(instance: HomeAssistantInstance) {
|
||||
if (adapter.servers.contains(instance)) {
|
||||
adapter.servers.remove(instance)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findExistingInstances() {
|
||||
Log.d(TAG, "findExistingInstances")
|
||||
Tasks.await(Wearable.getDataClient(this).getDataItems(Uri.parse("wear://*/home_assistant_instance"))).apply {
|
||||
Log.d(TAG, "findExistingInstances: success, found ${this.count}")
|
||||
this.forEach { item ->
|
||||
val instance = presenter.getInstance(DataMapItem.fromDataItem(item).dataMap)
|
||||
this@OnboardingActivity.runOnUiThread {
|
||||
onInstanceFound(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestInstances() {
|
||||
Log.d(TAG, "requestInstances")
|
||||
|
||||
// Find all nodes that are capable
|
||||
val capabilityInfo: CapabilityInfo = Tasks.await(
|
||||
Wearable.getCapabilityClient(this)
|
||||
.getCapability(
|
||||
"request_home_assistant_instance",
|
||||
CapabilityClient.FILTER_REACHABLE
|
||||
)
|
||||
)
|
||||
|
||||
if (capabilityInfo.nodes.size == 0) {
|
||||
Log.d(TAG, "requestInstances: No nodes found")
|
||||
}
|
||||
|
||||
capabilityInfo.nodes.forEach { node ->
|
||||
Wearable.getMessageClient(this).sendMessage(
|
||||
node.id,
|
||||
"/request_home_assistant_instance",
|
||||
ByteArray(0)
|
||||
).apply {
|
||||
addOnSuccessListener { Log.d(TAG, "requestInstances: request home assistant instances from $node.id: ${node.displayName}") }
|
||||
addOnFailureListener { Log.w(TAG, "requestInstances: failed to request home assistant instances from $node.id: ${node.displayName}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.onFinish()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.homeassistant.companion.android.onboarding
|
||||
|
||||
import com.google.android.gms.wearable.DataClient
|
||||
import com.google.android.gms.wearable.DataMap
|
||||
|
||||
interface OnboardingPresenter : DataClient.OnDataChangedListener {
|
||||
|
||||
fun onAdapterItemClick(instance: HomeAssistantInstance)
|
||||
|
||||
fun onFinish()
|
||||
|
||||
fun getInstance(map: DataMap): HomeAssistantInstance
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package io.homeassistant.companion.android.onboarding
|
||||
|
||||
import android.util.Log
|
||||
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 io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
|
||||
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowInit
|
||||
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.net.URL
|
||||
import javax.inject.Inject
|
||||
|
||||
class OnboardingPresenterImpl @Inject constructor(
|
||||
private val view: OnboardingView,
|
||||
private val authenticationUseCase: AuthenticationRepository,
|
||||
private val urlUseCase: UrlRepository
|
||||
) : OnboardingPresenter {
|
||||
companion object {
|
||||
private const val TAG = "OnboardingPresenter"
|
||||
}
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun onDataChanged(dataEvents: DataEventBuffer) {
|
||||
Log.d(TAG, "onDataChanged: [${dataEvents.count}]")
|
||||
dataEvents.forEach { event ->
|
||||
if (event.type == DataEvent.TYPE_CHANGED) {
|
||||
event.dataItem.also { item ->
|
||||
if (item.uri.path?.compareTo("/home_assistant_instance") == 0) {
|
||||
Log.d(TAG, "onDataChanged: found home_assistant_instance")
|
||||
val instance = getInstance(DataMapItem.fromDataItem(item).dataMap)
|
||||
view.onInstanceFound(instance)
|
||||
}
|
||||
}
|
||||
} else if (event.type == DataEvent.TYPE_DELETED) {
|
||||
event.dataItem.also { item ->
|
||||
if (item.uri.path?.compareTo("/home_assistant_instance") == 0) {
|
||||
val instance = getInstance(DataMapItem.fromDataItem(item).dataMap)
|
||||
view.onInstanceLost(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInstance(map: DataMap): HomeAssistantInstance {
|
||||
map.apply {
|
||||
return HomeAssistantInstance(
|
||||
getString("name", ""),
|
||||
URL(getString("url", "")),
|
||||
getString("version", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAdapterItemClick(instance: HomeAssistantInstance) {
|
||||
Log.d(TAG, "onAdapterItemClick: ${instance.name}")
|
||||
view.showLoading()
|
||||
mainScope.launch {
|
||||
// Set current url
|
||||
urlUseCase.saveUrl(instance.url.toString())
|
||||
|
||||
// Initiate login flow
|
||||
try {
|
||||
val flowInit: LoginFlowInit = authenticationUseCase.initiateLoginFlow()
|
||||
Log.d(TAG, "Created login flow step ${flowInit.stepId}: ${flowInit.flowId}")
|
||||
|
||||
view.startAuthentication(flowInit.flowId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initiate login flow", e)
|
||||
view.showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
mainScope.cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.homeassistant.companion.android.onboarding
|
||||
|
||||
interface OnboardingView {
|
||||
fun startAuthentication(flowId: String)
|
||||
fun startManualSetup()
|
||||
|
||||
fun onInstanceFound(instance: HomeAssistantInstance)
|
||||
fun onInstanceLost(instance: HomeAssistantInstance)
|
||||
|
||||
fun showLoading()
|
||||
|
||||
fun showError()
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package io.homeassistant.companion.android.onboarding
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.onboarding.viewHolders.HeaderViewHolder
|
||||
import io.homeassistant.companion.android.onboarding.viewHolders.InstanceViewHolder
|
||||
import io.homeassistant.companion.android.onboarding.viewHolders.LoadingViewHolder
|
||||
import io.homeassistant.companion.android.onboarding.viewHolders.ManualSetupViewHolder
|
||||
import kotlin.math.min
|
||||
|
||||
class ServerListAdapter(
|
||||
val servers: ArrayList<HomeAssistantInstance>
|
||||
) : RecyclerView.Adapter<ViewHolder>() {
|
||||
|
||||
lateinit var onInstanceClicked: (HomeAssistantInstance) -> Unit
|
||||
lateinit var onManualSetupClicked: () -> Unit
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ViewHolder {
|
||||
return when (viewType) {
|
||||
TYPE_INSTANCE -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.listitem_instance, parent, false)
|
||||
InstanceViewHolder(view, onInstanceClicked)
|
||||
}
|
||||
TYPE_HEADER -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.listitem_header, parent, false)
|
||||
HeaderViewHolder(view)
|
||||
}
|
||||
TYPE_MANUAL -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.listitem_instance, parent, false)
|
||||
ManualSetupViewHolder(view, onManualSetupClicked)
|
||||
}
|
||||
else -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.listitem_loading, parent, false)
|
||||
LoadingViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
if (holder is InstanceViewHolder && position <= servers.size) {
|
||||
holder.server = servers[position - 1]
|
||||
} else if (holder is ManualSetupViewHolder) {
|
||||
holder.text.setText(R.string.manual_setup)
|
||||
} else if (holder is HeaderViewHolder) {
|
||||
if (position == 0) {
|
||||
holder.headerTextView.setText(R.string.list_header_instances)
|
||||
} else {
|
||||
holder.headerTextView.setText(R.string.other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = min(servers.size + 3, 4)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when {
|
||||
position == 0 || position == this.itemCount - 2 -> TYPE_HEADER
|
||||
position == this.itemCount - 1 -> TYPE_MANUAL
|
||||
servers.size > 0 -> TYPE_INSTANCE
|
||||
else -> TYPE_LOADING
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
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 android.widget.EditText
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.wear.activity.ConfirmationActivity
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import io.homeassistant.companion.android.DaggerPresenterComponent
|
||||
import io.homeassistant.companion.android.PresenterModule
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity
|
||||
import io.homeassistant.companion.android.util.LoadingView
|
||||
import javax.inject.Inject
|
||||
|
||||
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 loadingView: LoadingView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (intent == null || !intent.hasExtra("flowId")) {
|
||||
Log.e(TAG, "Flow id not specified, canceling authentication")
|
||||
finish()
|
||||
}
|
||||
|
||||
DaggerPresenterComponent
|
||||
.builder()
|
||||
.appComponent((application as GraphComponentAccessor).appComponent)
|
||||
.presenterModule(PresenterModule(this))
|
||||
.build()
|
||||
.inject(this)
|
||||
|
||||
setContentView(R.layout.activity_authentication)
|
||||
|
||||
loadingView = findViewById<LoadingView>(R.id.loading_view)
|
||||
|
||||
findViewById<FloatingActionButton>(R.id.button_next).setOnClickListener {
|
||||
presenter.onNextClicked(
|
||||
intent.getStringExtra("flowId")!!,
|
||||
findViewById<EditText>(R.id.username).text.toString(),
|
||||
findViewById<EditText>(R.id.password).text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
loadingView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun startIntegration() {
|
||||
startActivity(MobileAppIntegrationActivity.newInstance(this))
|
||||
}
|
||||
|
||||
override fun showLoading() {
|
||||
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(R.string.failed_authentication))
|
||||
}
|
||||
startActivity(intent)
|
||||
loadingView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.onFinish()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.homeassistant.companion.android.onboarding.authentication
|
||||
|
||||
interface AuthenticationPresenter {
|
||||
|
||||
fun onNextClicked(flowId: String, username: String, password: String)
|
||||
|
||||
fun onFinish()
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package io.homeassistant.companion.android.onboarding.authentication
|
||||
|
||||
import android.util.Log
|
||||
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
|
||||
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowCreateEntry
|
||||
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(
|
||||
private val view: AuthenticationView,
|
||||
private val authenticationUseCase: AuthenticationRepository
|
||||
) : AuthenticationPresenter {
|
||||
companion object {
|
||||
private const val TAG = "AuthenticationPresenter"
|
||||
}
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun onNextClicked(flowId: String, username: String, password: String) {
|
||||
view.showLoading()
|
||||
Log.d(TAG, "onNextClicked")
|
||||
mainScope.launch {
|
||||
try {
|
||||
val flowCreateEntry: LoginFlowCreateEntry = authenticationUseCase.loginAuthentication(flowId, username, password)
|
||||
Log.d(TAG, "Authenticated result: ${flowCreateEntry.result}")
|
||||
authenticationUseCase.registerAuthorizationCode(flowCreateEntry.result)
|
||||
Log.d(TAG, "Finished!")
|
||||
|
||||
view.startIntegration()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate", e)
|
||||
view.showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
mainScope.cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.homeassistant.companion.android.onboarding.authentication
|
||||
|
||||
interface AuthenticationView {
|
||||
fun startIntegration()
|
||||
|
||||
fun showLoading()
|
||||
|
||||
fun showError()
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package io.homeassistant.companion.android.onboarding.integration
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.wear.activity.ConfirmationActivity
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import io.homeassistant.companion.android.DaggerPresenterComponent
|
||||
import io.homeassistant.companion.android.PresenterModule
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.home.HomeActivity
|
||||
import io.homeassistant.companion.android.util.LoadingView
|
||||
import javax.inject.Inject
|
||||
|
||||
class MobileAppIntegrationActivity : AppCompatActivity(), MobileAppIntegrationView {
|
||||
companion object {
|
||||
private const val TAG = "MobileAppIntegrationActivity"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, MobileAppIntegrationActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: MobileAppIntegrationPresenter
|
||||
private lateinit var loadingView: LoadingView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
DaggerPresenterComponent
|
||||
.builder()
|
||||
.appComponent((application as GraphComponentAccessor).appComponent)
|
||||
.presenterModule(PresenterModule(this))
|
||||
.build()
|
||||
.inject(this)
|
||||
|
||||
setContentView(R.layout.activity_integration)
|
||||
|
||||
loadingView = findViewById<LoadingView>(R.id.loading_view)
|
||||
|
||||
val serverUrl: EditText = findViewById(R.id.server_url)
|
||||
serverUrl.setText(Build.MODEL)
|
||||
|
||||
findViewById<FloatingActionButton>(R.id.finish).setOnClickListener {
|
||||
presenter.onRegistrationAttempt(serverUrl.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
loadingView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun deviceRegistered() {
|
||||
val intent = HomeActivity.newInstance(this)
|
||||
// empty the back stack
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun showLoading() {
|
||||
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(R.string.failed_registration))
|
||||
}
|
||||
startActivity(intent)
|
||||
loadingView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.onFinish()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.homeassistant.companion.android.onboarding.integration
|
||||
|
||||
interface MobileAppIntegrationPresenter {
|
||||
fun onRegistrationAttempt(deviceName: String)
|
||||
fun onFinish()
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package io.homeassistant.companion.android.onboarding.integration
|
||||
|
||||
import android.util.Log
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
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 MobileAppIntegrationPresenterImpl @Inject constructor(
|
||||
private val view: MobileAppIntegrationView,
|
||||
private val integrationUseCase: IntegrationRepository
|
||||
) : MobileAppIntegrationPresenter {
|
||||
|
||||
companion object {
|
||||
internal const val TAG = "IntegrationPresenter"
|
||||
}
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
private suspend fun createRegistration(deviceName: String): DeviceRegistration {
|
||||
return DeviceRegistration(
|
||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||
deviceName
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRegistrationAttempt(deviceName: String) {
|
||||
view.showLoading()
|
||||
mainScope.launch {
|
||||
val deviceRegistration = createRegistration(deviceName)
|
||||
try {
|
||||
integrationUseCase.registerDevice(deviceRegistration)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to register with Home Assistant", e)
|
||||
view.showError()
|
||||
return@launch
|
||||
}
|
||||
view.deviceRegistered()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
mainScope.cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package io.homeassistant.companion.android.onboarding.integration
|
||||
|
||||
interface MobileAppIntegrationView {
|
||||
|
||||
fun deviceRegistered()
|
||||
|
||||
fun showLoading()
|
||||
|
||||
fun showError()
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package io.homeassistant.companion.android.onboarding.manual_setup
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.wear.activity.ConfirmationActivity
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import io.homeassistant.companion.android.DaggerPresenterComponent
|
||||
import io.homeassistant.companion.android.PresenterModule
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
|
||||
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationActivity
|
||||
import io.homeassistant.companion.android.util.LoadingView
|
||||
import javax.inject.Inject
|
||||
|
||||
class ManualSetupActivity : AppCompatActivity(), ManualSetupView {
|
||||
companion object {
|
||||
private const val TAG = "ManualSetupActivity"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, ManualSetupActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: ManualSetupPresenter
|
||||
private lateinit var loadingView: LoadingView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
DaggerPresenterComponent
|
||||
.builder()
|
||||
.appComponent((application as GraphComponentAccessor).appComponent)
|
||||
.presenterModule(PresenterModule(this))
|
||||
.build()
|
||||
.inject(this)
|
||||
|
||||
setContentView(R.layout.activity_manual_setup)
|
||||
|
||||
loadingView = findViewById<LoadingView>(R.id.loading_view)
|
||||
|
||||
findViewById<FloatingActionButton>(R.id.button_next).setOnClickListener {
|
||||
presenter.onNextClicked(findViewById<EditText>(R.id.server_url).text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun startAuthentication(flowId: String) {
|
||||
startActivity(AuthenticationActivity.newInstance(this, flowId))
|
||||
}
|
||||
|
||||
override fun showLoading() {
|
||||
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(R.string.failed_connection))
|
||||
}
|
||||
startActivity(intent)
|
||||
loadingView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
loadingView.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.onFinish()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.homeassistant.companion.android.onboarding.manual_setup
|
||||
|
||||
interface ManualSetupPresenter {
|
||||
|
||||
fun onNextClicked(url: String)
|
||||
|
||||
fun onFinish()
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package io.homeassistant.companion.android.onboarding.manual_setup
|
||||
|
||||
import android.util.Log
|
||||
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
|
||||
import io.homeassistant.companion.android.common.data.authentication.impl.entities.LoginFlowInit
|
||||
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 javax.inject.Inject
|
||||
|
||||
class ManualSetupPresenterImpl @Inject constructor(
|
||||
private val view: ManualSetupView,
|
||||
private val authenticationUseCase: AuthenticationRepository,
|
||||
private val urlUseCase: UrlRepository
|
||||
) : ManualSetupPresenter {
|
||||
companion object {
|
||||
private const val TAG = "ManualSetupPresenter"
|
||||
}
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun onNextClicked(url: String) {
|
||||
view.showLoading()
|
||||
mainScope.launch {
|
||||
// Set current url
|
||||
urlUseCase.saveUrl(url)
|
||||
|
||||
// Initiate login flow
|
||||
try {
|
||||
val flowInit: LoginFlowInit = authenticationUseCase.initiateLoginFlow()
|
||||
Log.d(TAG, "Created login flow step ${flowInit.stepId}: ${flowInit.flowId}")
|
||||
|
||||
view.startAuthentication(flowInit.flowId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initiate login flow", e)
|
||||
view.showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
mainScope.cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.homeassistant.companion.android.onboarding.manual_setup
|
||||
|
||||
interface ManualSetupView {
|
||||
fun startAuthentication(flowId: String)
|
||||
|
||||
fun showLoading()
|
||||
|
||||
fun showError()
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package io.homeassistant.companion.android.onboarding.viewHolders
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.homeassistant.companion.android.R
|
||||
|
||||
class HeaderViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
val headerTextView = v.findViewById<TextView>(R.id.headerTextView)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package io.homeassistant.companion.android.onboarding.viewHolders
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.onboarding.HomeAssistantInstance
|
||||
|
||||
class InstanceViewHolder(v: View, val onClick: (HomeAssistantInstance) -> Unit) :
|
||||
RecyclerView.ViewHolder(v), View.OnClickListener {
|
||||
|
||||
private val name: TextView = v.findViewById(R.id.name)
|
||||
var server: HomeAssistantInstance? = null
|
||||
set(value) {
|
||||
name.text = value?.name
|
||||
field = value
|
||||
}
|
||||
|
||||
init {
|
||||
v.setOnClickListener {
|
||||
server?.let { onClick(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
Log.d("ServerListAdapter", "Clicked")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.homeassistant.companion.android.onboarding.viewHolders
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class LoadingViewHolder(v: View) : RecyclerView.ViewHolder(v)
|
|
@ -0,0 +1,19 @@
|
|||
package io.homeassistant.companion.android.onboarding.viewHolders
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.homeassistant.companion.android.R
|
||||
|
||||
class ManualSetupViewHolder(v: View, val onClick: () -> Unit) :
|
||||
RecyclerView.ViewHolder(v) {
|
||||
|
||||
val text: TextView = v.findViewById(R.id.name)
|
||||
|
||||
init {
|
||||
// Set onclick listener
|
||||
v.setOnClickListener {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.homeassistant.companion.android.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import io.homeassistant.companion.android.R
|
||||
|
||||
class LoadingView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.view_loading, this)
|
||||
|
||||
val loadingText: TextView = findViewById(R.id.loading_text)
|
||||
|
||||
val attributes = context.obtainStyledAttributes(attrs, R.styleable.LoadingView)
|
||||
loadingText.text = attributes.getString(R.styleable.LoadingView_loading_text)
|
||||
attributes.recycle()
|
||||
}
|
||||
}
|
10
wear/src/main/res/drawable/ic_button_arrow_forward.xml
Normal file
10
wear/src/main/res/drawable/ic_button_arrow_forward.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
|
||||
</vector>
|
10
wear/src/main/res/drawable/ic_button_check.xml
Normal file
10
wear/src/main/res/drawable/ic_button_check.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
6
wear/src/main/res/drawable/item_background.xml
Normal file
6
wear/src/main/res/drawable/item_background.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/colorOverlay" />
|
||||
<corners android:radius="32dp"/>
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
69
wear/src/main/res/layout/activity_authentication.xml
Normal file
69
wear/src/main/res/layout/activity_authentication.xml
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?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/textView2"
|
||||
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" />
|
||||
|
||||
<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/textView2" />
|
||||
|
||||
<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:importantForAutofill="no" />
|
||||
</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>
|
|
@ -1,42 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.wear.widget.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView 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:background="@color/dark_grey"
|
||||
android:padding="@dimen/box_inset_layout_padding"
|
||||
tools:context=".Home"
|
||||
android:fillViewport="true"
|
||||
tools:context=".home.HomeActivity"
|
||||
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">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="5dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="84dp"
|
||||
android:layout_height="84dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/app_icon" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/app_icon"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</androidx.wear.widget.BoxInsetLayout>
|
||||
<TextView
|
||||
android:id="@+id/txt_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:text="@string/loading"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txt_entities"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:text="@string/loading"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_logout"
|
||||
android:layout_height="48dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cornerRadius="24dp"
|
||||
android:backgroundTint="@color/colorWarning"
|
||||
android:text="@string/logout" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
60
wear/src/main/res/layout/activity_integration.xml
Normal file
60
wear/src/main/res/layout/activity_integration.xml
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?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.integration.MobileAppIntegrationActivity"
|
||||
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">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/server_url"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/device_name"
|
||||
android:inputType="textPersonName"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
style="@style/HeaderText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/registerDevice"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/finish"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
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_check" />
|
||||
|
||||
</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_registration"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.wear.widget.BoxInsetLayout>
|
58
wear/src/main/res/layout/activity_manual_setup.xml
Normal file
58
wear/src/main/res/layout/activity_manual_setup.xml
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?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: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">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/server_url"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/input_url_hint"
|
||||
android:inputType="textPersonName"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
style="@style/HeaderText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/choose_server"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<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" />
|
||||
|
||||
</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_connection"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.wear.widget.BoxInsetLayout>
|
24
wear/src/main/res/layout/activity_onboarding.xml
Normal file
24
wear/src/main/res/layout/activity_onboarding.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<FrameLayout
|
||||
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">
|
||||
<androidx.wear.widget.WearableRecyclerView
|
||||
android:id="@+id/server_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
tools:listitem="@layout/listitem_instance">
|
||||
</androidx.wear.widget.WearableRecyclerView>
|
||||
|
||||
<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_connection"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
19
wear/src/main/res/layout/listitem_header.xml
Normal file
19
wear/src/main/res/layout/listitem_header.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/headerTextView"
|
||||
style="@style/HeaderText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
tools:text="@string/list_header_instances" />
|
||||
|
||||
</RelativeLayout>
|
22
wear/src/main/res/layout/listitem_instance.xml
Normal file
22
wear/src/main/res/layout/listitem_instance.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/item_background"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:ellipsize="marquee"
|
||||
android:maxLines="1"
|
||||
tools:text="@string/input_url_hint" />
|
||||
|
||||
</RelativeLayout>
|
21
wear/src/main/res/layout/listitem_loading.xml
Normal file
21
wear/src/main/res/layout/listitem_loading.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:ellipsize="marquee"
|
||||
android:maxLines="1"
|
||||
tools:text="@string/loading" />
|
||||
|
||||
</RelativeLayout>
|
23
wear/src/main/res/layout/view_loading.xml
Normal file
23
wear/src/main/res/layout/view_loading.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- TODO: might want to wrap in SwipeDismissFrameLayout to be able to cancel loading -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:background="@color/colorActivityBackground">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/loading_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/attempting_authentication"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorDialogBackground">#111111</color>
|
||||
<color name="colorDialogMessage">@android:color/white</color>
|
||||
<color name="colorDialogTitle">@android:color/white</color>
|
||||
<color name="colorActivityBackground">#1c1c1c</color>
|
||||
<color name="colorPrimary">#03A9F4</color>
|
||||
<color name="colorPrimaryDark">#111111</color>
|
||||
<color name="colorAccent">#03A9F4</color>
|
||||
<color name="colorOnPrimary">@android:color/white</color>
|
||||
<color name="colorWarning">#D32F2F</color>
|
||||
<color name="colorHeadline1">@android:color/white</color>
|
||||
<color name="colorHeadline2">@android:color/white</color>
|
||||
<color name="colorActionBar">#1c1c1c</color>
|
||||
<color name="colorIcon">@android:color/white</color>
|
||||
<color name="colorWidgetButtonBackground">#1c1c1c</color>
|
||||
<color name="colorWidgetButtonLabel">#E6E6E6</color>
|
||||
</resources>
|
6
wear/src/main/res/values/attrs.xml
Normal file
6
wear/src/main/res/values/attrs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="LoadingView">
|
||||
<attr name="loading_text" format="string" />
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -1,18 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorDialogBackground">@android:color/white</color>
|
||||
<color name="colorDialogMessage">@android:color/black</color>
|
||||
<color name="colorDialogTitle">@android:color/black</color>
|
||||
<color name="colorActivityBackground">@android:color/white</color>
|
||||
<color name="colorDialogBackground">#111111</color>
|
||||
<color name="colorDialogMessage">@android:color/white</color>
|
||||
<color name="colorDialogTitle">@android:color/white</color>
|
||||
<color name="colorActivityBackground">#1c1c1c</color>
|
||||
<color name="colorPrimary">#03A9F4</color>
|
||||
<color name="colorPrimaryDark">#0288D1</color>
|
||||
<color name="colorPrimaryDark">#111111</color>
|
||||
<color name="colorAccent">#03A9F4</color>
|
||||
<color name="colorOnPrimary">@android:color/white</color>
|
||||
<color name="colorWarning">#D32F2F</color>
|
||||
<color name="colorHeadline1">@android:color/black</color>
|
||||
<color name="colorHeadline2">@android:color/black</color>
|
||||
<color name="colorActionBar">#03A9F4</color>
|
||||
<color name="colorIcon">@android:color/black</color>
|
||||
<color name="colorWidgetButtonBackground">@android:color/white</color>
|
||||
<color name="colorWidgetButtonLabel">#3A3A3A</color>
|
||||
<color name="colorHint">#70FFFFFF</color>
|
||||
<color name="colorHeadline1">@android:color/white</color>
|
||||
<color name="colorHeadline2">@android:color/white</color>
|
||||
<color name="colorActionBar">#1c1c1c</color>
|
||||
<color name="colorIcon">@android:color/white</color>
|
||||
<color name="colorOverlay">#24FFFFFF</color>
|
||||
<color name="colorWidgetButtonBackground">#1c1c1c</color>
|
||||
<color name="colorWidgetButtonLabel">#E6E6E6</color>
|
||||
</resources>
|
||||
|
|
|
@ -1,3 +1,30 @@
|
|||
<resources>
|
||||
<string name="app_name">Home Assistant Wear OS</string>
|
||||
<string name="app_name">Home Assistant</string>
|
||||
<string name="attempting_authentication">Authenticating…</string>
|
||||
<string name="attempting_connection">Connecting…</string>
|
||||
<string name="attempting_registration">Registering application…</string>
|
||||
<string name="button_forward">Next</string>
|
||||
<string name="choose_server">Choose server</string>
|
||||
<string name="device_name">Device Name</string>
|
||||
<plurals name="entities_found">
|
||||
<item quantity="one">%d entity found</item>
|
||||
<item quantity="other">%d entities found</item>
|
||||
</plurals>
|
||||
<string name="failed_authentication">Could not authenticate</string>
|
||||
<string name="failed_connection">Could not connect</string>
|
||||
<string name="failed_registration">Could not register</string>
|
||||
<string name="finish">Finish</string>
|
||||
<string name="input_url">Home Assistant URL</string>
|
||||
<string name="input_url_hint">https://example.duckdns.org:8123</string>
|
||||
<string name="list_header_instances">Instances</string>
|
||||
<string name="list_header_other">Other</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="logout">Logout</string>
|
||||
<string name="manual_setup">Manual setup</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="registerDevice">Register watch</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="version">Version: %s</string>
|
||||
</resources>
|
|
@ -1,10 +1,32 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.HomeAssistant" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorSecondary">@color/colorAccent</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimary</item>
|
||||
<item name="colorSurface">@color/colorOverlay</item>
|
||||
<item name="colorOnSurface">@color/colorAccent</item>
|
||||
<item name="android:colorBackground">@color/colorActivityBackground</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
|
||||
<item name="editTextStyle">@style/editText</item>
|
||||
</style>
|
||||
|
||||
<style name="editText" parent="@android:style/Widget.EditText">
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
<item name="android:textColorHint">@color/colorHint</item>
|
||||
<item name="android:backgroundTint">@color/colorAccent</item>
|
||||
<item name="android:paddingTop">4dp</item>
|
||||
<item name="android:paddingBottom">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="HeaderText" parent="android:Widget.TextView">
|
||||
<item name="android:textColor">@color/colorPrimary</item>
|
||||
<item name="android:textSize">20sp</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
7
wear/src/main/res/values/wear.xml
Normal file
7
wear/src/main/res/values/wear.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?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>authentication_token</item>
|
||||
</string-array>
|
||||
</resources>
|
Loading…
Reference in a new issue