mirror of
https://github.com/home-assistant/android
synced 2024-10-02 22:34:46 +00:00
Add Wear OS Settings section (#1796)
* 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
This commit is contained in:
parent
5c9a1f5ed1
commit
f7883bebc5
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
|
|
@ -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
app/src/full/res/layout/activity_settings_wear.xml
Executable file
42
app/src/full/res/layout/activity_settings_wear.xml
Executable file
|
@ -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
app/src/full/res/menu/menu_activity_settings_wear.xml
Executable file
10
app/src/full/res/menu/menu_activity_settings_wear.xml
Executable file
|
@ -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
app/src/full/res/values/wear.xml
Executable file
7
app/src/full/res/values/wear.xml
Executable file
|
@ -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>
|
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
|
|
@ -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 {
|
||||
|
|
9
app/src/main/res/drawable/ic_baseline_watch_24.xml
Executable file
9
app/src/main/res/drawable/ic_baseline_watch_24.xml
Executable file
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -57,6 +57,16 @@
|
|||
app:isPreferenceVisible="false"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="wear_category"
|
||||
app:isPreferenceVisible="false"
|
||||
android:title="@string/wear_os_category">
|
||||
<Preference
|
||||
android:key="wear_settings"
|
||||
android:title="@string/wear_os_settings_title"
|
||||
android:icon="@drawable/ic_baseline_watch_24"
|
||||
android:summary="@string/wear_os_settings_summary" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/other_settings">
|
||||
<SwitchPreference
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package io.homeassistant.companion.android.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.gms.wearable.CapabilityClient
|
||||
import com.google.android.gms.wearable.CapabilityInfo
|
||||
|
||||
class SettingsWearActivity : AppCompatActivity(), CapabilityClient.OnCapabilityChangedListener {
|
||||
|
||||
override fun onCapabilityChanged(capabilityInfo: CapabilityInfo) {
|
||||
// No op
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsWearAct"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, SettingsWearActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,5 +3,6 @@
|
|||
tools:keep="@array/android_wear_capabilities">
|
||||
<string-array name="android_wear_capabilities">
|
||||
<item>authentication_token</item>
|
||||
<item>verify_wear_app</item>
|
||||
</string-array>
|
||||
</resources>
|
Loading…
Reference in a new issue