Add 'Sign in on phone' button to Wear OS login (#2154)

* Add 'Sign in on phone' button to Wear OS login

 - Detects a compatible app and shows a button to sign in on your phone instead, which is a lot easier than typing everything on the watch

* Include exception for phone sign in in log
This commit is contained in:
Joris Pelgröm 2022-01-18 22:40:17 +01:00 committed by GitHub
parent 9a28be2699
commit e1e12dc2a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 111 additions and 10 deletions

View file

@ -5,6 +5,7 @@
<item>verify_phone_app</item>
<item>request_authentication_token</item>
<item>request_home_assistant_instance</item>
<item>sign_in_to_home_assistant_instance</item>
<item>send_home_favorites</item>
<item>save_home_favorites</item>
</string-array>

View file

@ -261,7 +261,18 @@
<activity
android:name=".settings.wear.SettingsWearActivity"
android:parentActivityName=".settings.SettingsActivity"
android:configChanges="orientation|screenSize" />
android:configChanges="orientation|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="homeassistant"
android:host="wear-phone-signin" />
</intent-filter>
</activity>
<activity
android:name=".settings.wear.views.SettingsWearMainView"

View file

@ -110,6 +110,7 @@
<string name="confirm_delete_this_widget_title">Confirm deleting this widget</string>
<string name="confirm_negative">NO</string>
<string name="confirm_positive">YES</string>
<string name="continue_on_phone">Continue on your phone</string>
<string name="connect_to_home_assistant">Connect to Home Assistant</string>
<string name="connect_to_home_internet">Make sure your phone is connected\nto your home internet.</string>
<string name="connect">Connect</string>
@ -280,7 +281,7 @@
<string name="manage_widgets_summary">Edit your widgets, adding/deleting can only be done from the home screen</string>
<string name="manage_widgets">Manage Widgets</string>
<string name="manual_desc">Enter the URL of your Home Assistant server. Make sure the URL includes the protocol and port. For example:\n\nhttp://homeassistant.local:8123 or \nhttps://example.duckdns.org.</string>
<string name="manual_setup">enter address manually</string>
<string name="manual_setup">Enter address manually</string>
<string name="manual_title">What is your Home Assistant URL?</string>
<string name="map">Map</string>
<string name="maximum">Maximum</string>
@ -584,6 +585,7 @@
<string name="shortcuts">Shortcuts</string>
<string name="show_share_logs_summary">Sharing logs with the Home Assistant team will help to solve issues. Please share the logs only if you have been asked to do so by a Home Assistant developer</string>
<string name="show_share_logs">Show and Share Logs</string>
<string name="sign_in_on_phone">Sign in on phone</string>
<string name="skip">Skip</string>
<string name="state_auto">Auto</string>
<string name="state_cleaning">Cleaning</string>

View file

@ -95,6 +95,7 @@ dependencies {
implementation("com.google.android.support:wearable:2.8.1")
implementation("com.google.android.gms:play-services-wearable:17.1.0")
implementation("androidx.wear:wear-input:1.2.0-alpha02")
implementation("androidx.wear:wear-remote-interactions:1.0.0")
compileOnly("com.google.android.wearable:wearable:2.8.1")
implementation("com.google.dagger:hilt-android:2.40.5")

View file

@ -9,6 +9,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.wear.activity.ConfirmationActivity
import androidx.wear.remote.interactions.RemoteActivityHelper
import androidx.wear.widget.WearableRecyclerView
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.wearable.CapabilityClient
@ -28,6 +29,9 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
private lateinit var adapter: ServerListAdapter
private lateinit var capabilityClient: CapabilityClient
private lateinit var remoteActivityHelper: RemoteActivityHelper
companion object {
private const val TAG = "OnboardingActivity"
@ -45,11 +49,15 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
setContentView(R.layout.activity_onboarding)
loadingView = findViewById<LoadingView>(R.id.loading_view)
loadingView = findViewById(R.id.loading_view)
adapter = ServerListAdapter(ArrayList())
adapter.onInstanceClicked = { instance -> presenter.onAdapterItemClick(instance) }
adapter.onManualSetupClicked = { this.startManualSetup() }
adapter.onPhoneSignInClicked = { this.startPhoneSignIn() }
capabilityClient = Wearable.getCapabilityClient(this)
remoteActivityHelper = RemoteActivityHelper(this)
findViewById<WearableRecyclerView>(R.id.server_list)?.apply {
layoutManager = LinearLayoutManager(this@OnboardingActivity)
@ -71,6 +79,9 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
// Request authentication token in separate task
Thread { requestInstances() }.start()
// Check if there is a phone connected that supports sign in
Thread { requestPhoneSignIn() }.start()
}
override fun onPause() {
@ -87,6 +98,28 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
startActivity(ManualSetupActivity.newInstance(this))
}
override fun startPhoneSignIn() {
try {
remoteActivityHelper.startRemoteActivity(
Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_DEFAULT)
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse("homeassistant://wear-phone-signin")
},
null // a Wear device only has one companion device so this is not needed
)
val confirmation = Intent(this, ConfirmationActivity::class.java).apply {
putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.OPEN_ON_PHONE_ANIMATION)
putExtra(ConfirmationActivity.EXTRA_ANIMATION_DURATION_MILLIS, 2500)
putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(commonR.string.continue_on_phone))
}
startActivity(confirmation)
} catch (e: Exception) {
Log.e(TAG, "Unable to open sign in activity on phone", e)
showError()
}
}
override fun showLoading() {
loadingView.visibility = View.VISIBLE
}
@ -135,11 +168,10 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
// Find all nodes that are capable
val capabilityInfo: CapabilityInfo = Tasks.await(
Wearable.getCapabilityClient(this)
.getCapability(
"request_home_assistant_instance",
CapabilityClient.FILTER_REACHABLE
)
capabilityClient.getCapability(
"request_home_assistant_instance",
CapabilityClient.FILTER_REACHABLE
)
)
if (capabilityInfo.nodes.size == 0) {
@ -158,6 +190,24 @@ class OnboardingActivity : AppCompatActivity(), OnboardingView {
}
}
private fun requestPhoneSignIn() {
Log.d(TAG, "requestPhoneSignIn")
// Find all nodes that are capable
val capabilityInfo: CapabilityInfo = Tasks.await(
capabilityClient.getCapability(
"sign_in_to_home_assistant_instance",
CapabilityClient.FILTER_REACHABLE
)
)
Log.d(TAG, "requestPhoneSignIn: found ${capabilityInfo.nodes.size} nodes")
runOnUiThread {
adapter.phoneSignInAvailable = capabilityInfo.nodes.size > 0
adapter.notifyDataSetChanged()
}
}
override fun onDestroy() {
presenter.onFinish()
super.onDestroy()

View file

@ -3,6 +3,7 @@ package io.homeassistant.companion.android.onboarding
interface OnboardingView {
fun startAuthentication(flowId: String)
fun startManualSetup()
fun startPhoneSignIn()
fun onInstanceFound(instance: HomeAssistantInstance)
fun onInstanceLost(instance: HomeAssistantInstance)

View file

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

View file

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