Skip to content

Commit

Permalink
Add Wear OS Settings section (#1796)
Browse files Browse the repository at this point in the history
* Add Wear OS Settings section

* Fix minimal build

* Fix PR build and hide category by default

* Mention Wear OS app is only offered as a beta in case user is unable to install

* Review comments
  • Loading branch information
dshokouhi authored Oct 21, 2021
1 parent 5c9a1f5 commit f7883be
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 1 deletion.
5 changes: 4 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.31")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")

implementation("com.google.dagger:dagger:2.39.1")
kapt("com.google.dagger:dagger-compiler:2.39.1")
Expand All @@ -146,6 +146,8 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:2.3.5")
implementation("com.google.android.material:material:1.4.0")

implementation("androidx.wear:wear-remote-interactions:1.0.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01")
implementation("com.google.android.gms:play-services-wearable:17.1.0")

implementation("androidx.room:room-runtime:2.3.0")
Expand All @@ -161,6 +163,7 @@ dependencies {
"fullImplementation"("com.google.firebase:firebase-iid:21.1.0")
"fullImplementation"("com.google.firebase:firebase-messaging:22.0.0")
"fullImplementation"("io.sentry:sentry-android:5.2.3")
"fullImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.2")

implementation("androidx.work:work-runtime-ktx:2.6.0")
implementation("androidx.biometric:biometric:1.1.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package io.homeassistant.companion.android.settings

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.wear.remote.interactions.RemoteActivityHelper
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.CapabilityInfo
import com.google.android.gms.wearable.Node
import com.google.android.gms.wearable.NodeClient
import com.google.android.gms.wearable.Wearable
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.databinding.ActivitySettingsWearBinding
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext

class SettingsWearActivity : AppCompatActivity(), CapabilityClient.OnCapabilityChangedListener {

private lateinit var binding: ActivitySettingsWearBinding

private lateinit var capabilityClient: CapabilityClient
private lateinit var nodeClient: NodeClient
private lateinit var remoteActivityHelper: RemoteActivityHelper

private var wearNodesWithApp: Set<Node>? = null
private var allConnectedNodes: List<Node>? = null

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_activity_settings_wear, menu)
return true
}

override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.get_help)?.let {
it.isVisible = true
it.intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://companion.home-assistant.io/docs/wear-os/wear-os"))
}
return true
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivitySettingsWearBinding.inflate(layoutInflater)
setContentView(binding.root)

setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
capabilityClient = Wearable.getCapabilityClient(this)
nodeClient = Wearable.getNodeClient(this)
remoteActivityHelper = RemoteActivityHelper(this)

binding.remoteOpenButton.setOnClickListener {
openPlayStoreOnWearDevicesWithoutApp()
}

// Perform the initial update of the UI
updateUI()

lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch {
// Initial request for devices with our capability, aka, our Wear app installed.
findWearDevicesWithApp()
}
launch {
// Initial request for all Wear devices connected (with or without our capability).
// Additional Note: Because there isn't a listener for ALL Nodes added/removed from network
// that isn't deprecated, we simply update the full list when the Google API Client is
// connected and when capability changes come through in the onCapabilityChanged() method.
findAllWearDevices()
}
}
}
}

override fun onPause() {
super.onPause()
capabilityClient.removeListener(this, CAPABILITY_WEAR_APP)
}

override fun onResume() {
super.onResume()
capabilityClient.addListener(this, CAPABILITY_WEAR_APP)
}

/*
* Updates UI when capabilities change (install/uninstall wear app).
*/
override fun onCapabilityChanged(capabilityInfo: CapabilityInfo) {
wearNodesWithApp = capabilityInfo.nodes

lifecycleScope.launch {
// Because we have an updated list of devices with/without our app, we need to also update
// our list of active Wear devices.
findAllWearDevices()
}
}

private suspend fun findWearDevicesWithApp() {

try {
val capabilityInfo = capabilityClient
.getCapability(CAPABILITY_WEAR_APP, CapabilityClient.FILTER_ALL)
.await()

withContext(Dispatchers.Main) {
wearNodesWithApp = capabilityInfo.nodes
Log.d(TAG, "Capable Nodes: $wearNodesWithApp")
updateUI()
}
} catch (cancellationException: CancellationException) {
// Request was cancelled normally
throw cancellationException
} catch (throwable: Throwable) {
Log.d(TAG, "Capability request failed to return any results.")
}
}

private suspend fun findAllWearDevices() {

try {
val connectedNodes = nodeClient.connectedNodes.await()

withContext(Dispatchers.Main) {
allConnectedNodes = connectedNodes
updateUI()
}
} catch (cancellationException: CancellationException) {
// Request was cancelled normally
} catch (throwable: Throwable) {
Log.d(TAG, "Node request failed to return any results.")
}
}

private fun updateUI() {

val wearNodesWithApp = wearNodesWithApp
val allConnectedNodes = allConnectedNodes

when {
wearNodesWithApp == null || allConnectedNodes == null -> {
Log.d(TAG, "Waiting on Results for both connected nodes and nodes with app")
binding.informationTextView.text = getString(R.string.message_checking)
binding.remoteOpenButton.isInvisible = true
}
allConnectedNodes.isEmpty() -> {
Log.d(TAG, "No devices")
binding.informationTextView.text = getString(R.string.message_checking)
binding.remoteOpenButton.isInvisible = true
}
wearNodesWithApp.isEmpty() -> {
Log.d(TAG, "Missing on all devices")
binding.informationTextView.text = getString(R.string.message_missing_all)
binding.remoteOpenButton.isVisible = true
}
wearNodesWithApp.size < allConnectedNodes.size -> {
// TODO: Add your code to communicate with the wear app(s) via Wear APIs
// (MessageClient, DataClient, etc.)
Log.d(TAG, "Installed on some devices")
binding.informationTextView.text =
getString(R.string.message_some_installed, wearNodesWithApp.toString())
binding.remoteOpenButton.isVisible = true
}
else -> {
// TODO: Add your code to communicate with the wear app(s) via Wear APIs
// (MessageClient, DataClient, etc.)
Log.d(TAG, "Installed on all devices")
binding.informationTextView.text =
getString(R.string.message_all_installed)
binding.remoteOpenButton.isInvisible = true
}
}
}

private fun openPlayStoreOnWearDevicesWithoutApp() {

val wearNodesWithApp = wearNodesWithApp ?: return
val allConnectedNodes = allConnectedNodes ?: return

// Determine the list of nodes (wear devices) that don't have the app installed yet.
val nodesWithoutApp = allConnectedNodes - wearNodesWithApp

Log.d(TAG, "Number of nodes without app: " + nodesWithoutApp.size)
val intent = Intent(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(Uri.parse(PLAY_STORE_APP_URI))

// In parallel, start remote activity requests for all wear devices that don't have the app installed yet.
nodesWithoutApp.forEach { node ->
lifecycleScope.launch {
try {
remoteActivityHelper
.startRemoteActivity(
targetIntent = intent,
targetNodeId = node.id
)
.await()

Toast.makeText(
this@SettingsWearActivity,
getString(R.string.store_request_successful),
Toast.LENGTH_SHORT
).show()
} catch (cancellationException: CancellationException) {
// Request was cancelled normally
} catch (throwable: Throwable) {
Toast.makeText(
this@SettingsWearActivity,
getString(R.string.store_request_unsuccessful),
Toast.LENGTH_LONG
).show()
}
}
}
}

companion object {
private const val TAG = "SettingsWearAct"

// Name of capability listed in Wear app's wear.xml.
// IMPORTANT NOTE: This should be named differently than your Phone app's capability.
private const val CAPABILITY_WEAR_APP = "verify_wear_app"

private const val PLAY_STORE_APP_URI =
"market://details?id=io.homeassistant.companion.android"

fun newInstance(context: Context): Intent {
return Intent(context, SettingsWearActivity::class.java)
}
}
}
42 changes: 42 additions & 0 deletions app/src/full/res/layout/activity_settings_wear.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent"
android:theme="@style/ThemeOverlay.HomeAssistant.ActionBar" />

<TextView
android:id="@+id/information_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:layout_marginStart="15dp"
android:autoLink="web"
android:layout_marginEnd="15dp"
style="@style/TextAppearance.HomeAssistant.Headline"
app:layout_constraintBottom_toTopOf="@id/remote_open_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside" />

<Button
android:id="@+id/remote_open_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/install_app"
android:visibility="invisible"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/information_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
10 changes: 10 additions & 0 deletions app/src/full/res/menu/menu_activity_settings_wear.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/get_help"
android:title="@string/get_help"
android:icon="@drawable/ic_question_toolbar"
android:visible="false"
app:showAsAction="always"/>
</menu>
7 changes: 7 additions & 0 deletions app/src/full/res/values/wear.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="android_wear_capabilities">
<!-- IMPORTANT NOTE: Should be different than capability in Wear res/values/wear.xml. -->
<item>verify_phone_app</item>
</string-array>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="io.homeassistant.companion.android">

<uses-sdk tools:overrideLibrary="androidx.wear.remote.interactions" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
Expand Down Expand Up @@ -233,6 +235,11 @@
android:name=".onboarding.OnboardingActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />

<activity
android:name=".settings.SettingsWearActivity"
android:parentActivityName=".settings.SettingsActivity"
android:configChanges="orientation|screenSize" />

<service android:name=".onboarding.WearOnboardingListener">
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ class SettingsFragment : PreferenceFragmentCompat(), SettingsView {
true
}
}

val pm = requireContext().packageManager
val hasWearApp = pm.getLaunchIntentForPackage("com.google.android.wearable.app")
findPreference<PreferenceCategory>("wear_category")?.isVisible = hasWearApp != null
findPreference<Preference>("wear_settings")?.setOnPreferenceClickListener {
startActivity(SettingsWearActivity.newInstance(requireContext()))
return@setOnPreferenceClickListener true
}
}

findPreference<Preference>("changelog")?.let {
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_baseline_watch_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/colorAccent"
android:pathData="M20,12c0,-2.54 -1.19,-4.81 -3.04,-6.27L16,0H8l-0.95,5.73C5.19,7.19 4,9.45 4,12s1.19,4.81 3.05,6.27L8,24h8l0.96,-5.73C18.81,16.81 20,14.54 20,12zM6,12c0,-3.31 2.69,-6 6,-6s6,2.69 6,6 -2.69,6 -6,6 -6,-2.69 -6,-6z"/>
</vector>
10 changes: 10 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,14 @@ like to connect to:</string>
<string name="prioritize_internal_summary">Always try the internal URL first before the external URL. Enable this setting if you typically leave location off.</string>
<string name="autoplay_video">Autoplay Videos</string>
<string name="autoplay_video_summary">Autoplay Videos when lovelace dashboard is active. Enabling this setting may increase data usage unexpectedly, proceed with caution.</string>
<string name="message_checking">Checking Wear Devices with App</string>
<string name="message_missing_all">The Wear app is missing on your watch, click the button below to install the app.\n\nNote: Currently the Wear OS app requires you to be enrolled in the beta for the phone app. If the button does not work then please join the beta: https://play.google.com/apps/testing/io.homeassistant.companion.android</string>
<string name="message_some_installed">The Wear app is installed on some of your wear devices: (%1$s)\n\nClick the button below to install the app on the other devices.\n\nNote: Currently the Wear OS app requires you to be enrolled in the beta for the phone app. If the button does not work then please join the beta: https://play.google.com/apps/testing/io.homeassistant.companion.android</string>
<string name="message_all_installed">The Wear app is installed on all of your wear devices! \n\nStay tuned for more updates to this page.</string>
<string name="store_request_successful">Request to install app on wear device sent successfully</string>
<string name="store_request_unsuccessful">Play Store Request Failed. Wear device(s) may not support Play Store, that is, the Wear device may be version 1.0.</string>
<string name="install_app">Install App on Wear Device</string>
<string name="wear_os_category">Wear OS</string>
<string name="wear_os_settings_title">Wear OS Settings</string>
<string name="wear_os_settings_summary">Manage Wear OS App</string>
</resources>
Loading

0 comments on commit f7883be

Please sign in to comment.