Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Wear OS Settings section #1796

Merged
merged 5 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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