-
-
Notifications
You must be signed in to change notification settings - Fork 674
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
12 changed files
with
376 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
246 changes: 246 additions & 0 deletions
246
app/src/full/java/io/homeassistant/companion/android/settings/SettingsWearActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.