mirror of
https://github.com/home-assistant/android
synced 2024-10-02 22:34:46 +00:00
Allow Phone to Onboard Wear Device (#2002)
* Initial data flowing. * Functional login from phone. * Cleanup data transfer between devices. * Fix minimal build. * Address review comments. * Defaults to actual device when logging in now. Location hidden when logging into wear device.
This commit is contained in:
parent
81975c77f3
commit
8c5149ddcb
|
@ -1,174 +0,0 @@
|
|||
package io.homeassistant.companion.android.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.android.gms.wearable.CapabilityClient
|
||||
import com.google.android.gms.wearable.DataClient
|
||||
import com.google.android.gms.wearable.DataEvent
|
||||
import com.google.android.gms.wearable.DataEventBuffer
|
||||
import com.google.android.gms.wearable.DataMap
|
||||
import com.google.android.gms.wearable.DataMapItem
|
||||
import com.google.android.gms.wearable.PutDataMapRequest
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.homeassistant.companion.android.HomeAssistantApplication
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
import org.burnoutcrew.reorderable.move
|
||||
import org.json.JSONArray
|
||||
import javax.inject.Inject
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsWearViewModel @Inject constructor(
|
||||
private val integrationUseCase: IntegrationRepository,
|
||||
application: Application
|
||||
) :
|
||||
AndroidViewModel(application),
|
||||
DataClient.OnDataChangedListener {
|
||||
|
||||
private val application = getApplication<HomeAssistantApplication>()
|
||||
companion object {
|
||||
private const val TAG = "SettingsWearViewModel"
|
||||
private const val CAPABILITY_WEAR_FAVORITES = "send_home_favorites"
|
||||
}
|
||||
|
||||
var entities = mutableStateMapOf<String, Entity<*>>()
|
||||
private set
|
||||
var favoriteEntityIds = mutableStateListOf<String>()
|
||||
|
||||
fun init() {
|
||||
loadEntities()
|
||||
}
|
||||
|
||||
private fun loadEntities() {
|
||||
viewModelScope.launch {
|
||||
integrationUseCase.getEntities().forEach {
|
||||
entities[it.entityId] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveHomeFavorites(data: String) {
|
||||
val jsonString = JSONArray(data)
|
||||
Log.d(TAG, "saveHomeFavorites: $jsonString")
|
||||
favoriteEntityIds.clear()
|
||||
for (favorite in 0 until jsonString.length()) {
|
||||
favoriteEntityIds.add(
|
||||
jsonString.getString(favorite)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEntitySelected(checked: Boolean, entityId: String) {
|
||||
if (checked)
|
||||
favoriteEntityIds.add(entityId)
|
||||
else
|
||||
favoriteEntityIds.remove(entityId)
|
||||
sendHomeFavorites(favoriteEntityIds.toList())
|
||||
}
|
||||
|
||||
fun onMove(fromItem: ItemPosition, toItem: ItemPosition) {
|
||||
favoriteEntityIds.move(favoriteEntityIds.indexOfFirst { it == fromItem.key }, favoriteEntityIds.indexOfFirst { it == toItem.key })
|
||||
}
|
||||
|
||||
fun canDragOver(position: ItemPosition) = favoriteEntityIds.any { it == position.key }
|
||||
|
||||
fun sendHomeFavorites(favoritesList: List<String>) = viewModelScope.launch {
|
||||
Log.d(TAG, "sendHomeFavorites")
|
||||
|
||||
val putDataRequest = PutDataMapRequest.create("/save_home_favorites").run {
|
||||
dataMap.putString("favorites", favoritesList.toString())
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
||||
Wearable.getDataClient(application).putDataItem(putDataRequest).apply {
|
||||
addOnSuccessListener { Log.d(TAG, "Successfully sent favorites to wear") }
|
||||
addOnFailureListener { e ->
|
||||
Log.e(TAG, "Failed to send favorites to wear", e)
|
||||
Toast.makeText(application, application.getString(commonR.string.failure_send_favorites_wear), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findExistingFavorites() {
|
||||
Log.d(TAG, "Finding existing favorites")
|
||||
viewModelScope.launch {
|
||||
Wearable.getDataClient(application).getDataItems(Uri.parse("wear://*/home_favorites"))
|
||||
.addOnSuccessListener { dataItemBuffer ->
|
||||
Log.d(TAG, "Found existing favorites: ${dataItemBuffer.count}")
|
||||
dataItemBuffer.forEach {
|
||||
val data = getFavorites(DataMapItem.fromDataItem(it).dataMap)
|
||||
saveHomeFavorites(data)
|
||||
}
|
||||
dataItemBuffer.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFavorites(map: DataMap): String {
|
||||
return map.getString("favorites", "")
|
||||
}
|
||||
|
||||
fun requestFavorites() {
|
||||
Log.d(TAG, "Requesting favorites")
|
||||
|
||||
viewModelScope.launch {
|
||||
Wearable.getCapabilityClient(application)
|
||||
.getCapability(CAPABILITY_WEAR_FAVORITES, CapabilityClient.FILTER_REACHABLE)
|
||||
.addOnSuccessListener {
|
||||
|
||||
it.nodes.forEach { node ->
|
||||
Log.d(TAG, "Requesting favorite data")
|
||||
Wearable.getMessageClient(application).sendMessage(
|
||||
node.id,
|
||||
"/send_home_favorites",
|
||||
ByteArray(0)
|
||||
).apply {
|
||||
addOnSuccessListener {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Request to favorites sent successfully"
|
||||
)
|
||||
}
|
||||
addOnFailureListener { e ->
|
||||
Log.e(TAG, "Failed to get favorites", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startWearListening() {
|
||||
Wearable.getDataClient(application).addListener(this)
|
||||
}
|
||||
|
||||
fun stopWearListening() {
|
||||
Wearable.getDataClient(application).removeListener(this)
|
||||
}
|
||||
|
||||
override fun onDataChanged(dataEvents: DataEventBuffer) {
|
||||
Log.d(TAG, "onDataChanged ${dataEvents.count}")
|
||||
dataEvents.forEach { event ->
|
||||
if (event.type == DataEvent.TYPE_CHANGED) {
|
||||
event.dataItem.also { item ->
|
||||
if (item.uri.path?.compareTo("/home_favorites") == 0) {
|
||||
val data = getFavorites(DataMapItem.fromDataItem(item).dataMap)
|
||||
saveHomeFavorites(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataEvents.release()
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package io.homeassistant.companion.android.settings.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.gms.wearable.Node
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.settings.SettingsWearViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsWearMainView : AppCompatActivity() {
|
||||
|
||||
private val settingsWearViewModel by viewModels<SettingsWearViewModel>()
|
||||
|
||||
@Inject
|
||||
lateinit var integrationUseCase: IntegrationRepository
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsWearDevice"
|
||||
private var currentNodes = setOf<Node>()
|
||||
const val LANDING = "Landing"
|
||||
const val FAVORITES = "Favorites"
|
||||
|
||||
fun newInstance(context: Context, wearNodes: Set<Node>): Intent {
|
||||
currentNodes = wearNodes
|
||||
return Intent(context, SettingsWearMainView::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
LoadSettingsHomeView(settingsWearViewModel, currentNodes.first().displayName)
|
||||
}
|
||||
settingsWearViewModel.init()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
settingsWearViewModel.startWearListening()
|
||||
settingsWearViewModel.findExistingFavorites()
|
||||
settingsWearViewModel.requestFavorites()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
settingsWearViewModel.stopWearListening()
|
||||
}
|
||||
}
|
|
@ -1,242 +1,242 @@
|
|||
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 io.homeassistant.companion.android.settings.views.SettingsWearMainView
|
||||
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
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
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(binding.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(commonR.string.message_checking)
|
||||
binding.remoteOpenButton.isInvisible = true
|
||||
}
|
||||
allConnectedNodes.isEmpty() -> {
|
||||
Log.d(TAG, "No devices")
|
||||
binding.informationTextView.text = getString(commonR.string.message_checking)
|
||||
binding.remoteOpenButton.isInvisible = true
|
||||
}
|
||||
wearNodesWithApp.isEmpty() -> {
|
||||
Log.d(TAG, "Missing on all devices")
|
||||
binding.informationTextView.text = getString(commonR.string.message_missing_all)
|
||||
binding.remoteOpenButton.isVisible = true
|
||||
}
|
||||
wearNodesWithApp.size < allConnectedNodes.size -> {
|
||||
Log.d(TAG, "Installed on some devices")
|
||||
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp))
|
||||
finish()
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Installed on all devices")
|
||||
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(commonR.string.store_request_successful),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (cancellationException: CancellationException) {
|
||||
// Request was cancelled normally
|
||||
} catch (throwable: Throwable) {
|
||||
Toast.makeText(
|
||||
this@SettingsWearActivity,
|
||||
getString(commonR.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
package io.homeassistant.companion.android.settings.wear
|
||||
|
||||
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 io.homeassistant.companion.android.settings.wear.views.SettingsWearMainView
|
||||
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
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
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(binding.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(commonR.string.message_checking)
|
||||
binding.remoteOpenButton.isInvisible = true
|
||||
}
|
||||
allConnectedNodes.isEmpty() -> {
|
||||
Log.d(TAG, "No devices")
|
||||
binding.informationTextView.text = getString(commonR.string.message_checking)
|
||||
binding.remoteOpenButton.isInvisible = true
|
||||
}
|
||||
wearNodesWithApp.isEmpty() -> {
|
||||
Log.d(TAG, "Missing on all devices")
|
||||
binding.informationTextView.text = getString(commonR.string.message_missing_all)
|
||||
binding.remoteOpenButton.isVisible = true
|
||||
}
|
||||
wearNodesWithApp.size < allConnectedNodes.size -> {
|
||||
Log.d(TAG, "Installed on some devices")
|
||||
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp))
|
||||
finish()
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Installed on all devices")
|
||||
startActivity(SettingsWearMainView.newInstance(applicationContext, wearNodesWithApp))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(commonR.string.store_request_successful),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (cancellationException: CancellationException) {
|
||||
// Request was cancelled normally
|
||||
} catch (throwable: Throwable) {
|
||||
Toast.makeText(
|
||||
this@SettingsWearActivity,
|
||||
getString(commonR.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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package io.homeassistant.companion.android.settings.wear
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.android.gms.wearable.CapabilityClient
|
||||
import com.google.android.gms.wearable.DataClient
|
||||
import com.google.android.gms.wearable.DataEvent
|
||||
import com.google.android.gms.wearable.DataEventBuffer
|
||||
import com.google.android.gms.wearable.DataMap
|
||||
import com.google.android.gms.wearable.DataMapItem
|
||||
import com.google.android.gms.wearable.PutDataMapRequest
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.homeassistant.companion.android.HomeAssistantApplication
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
import org.burnoutcrew.reorderable.move
|
||||
import javax.inject.Inject
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsWearViewModel @Inject constructor(
|
||||
private val integrationUseCase: IntegrationRepository,
|
||||
application: Application
|
||||
) :
|
||||
AndroidViewModel(application),
|
||||
DataClient.OnDataChangedListener {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsWearViewModel"
|
||||
private const val CAPABILITY_WEAR_SENDS_CONFIG = "sends_config"
|
||||
|
||||
private const val KEY_UPDATE_TIME = "UpdateTime"
|
||||
private const val KEY_IS_AUTHENTICATED = "isAuthenticated"
|
||||
private const val KEY_FAVORITES = "favorites"
|
||||
}
|
||||
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
|
||||
var hasData = mutableStateOf(false)
|
||||
private set
|
||||
var isAuthenticated = mutableStateOf(false)
|
||||
private set
|
||||
var entities = mutableStateMapOf<String, Entity<*>>()
|
||||
private set
|
||||
var favoriteEntityIds = mutableStateListOf<String>()
|
||||
private set
|
||||
|
||||
init {
|
||||
Wearable.getDataClient(application).addListener(this)
|
||||
Wearable.getCapabilityClient(application)
|
||||
.getCapability(CAPABILITY_WEAR_SENDS_CONFIG, CapabilityClient.FILTER_REACHABLE)
|
||||
.addOnSuccessListener {
|
||||
it.nodes.forEach { node ->
|
||||
Log.d(TAG, "Requesting config from node ${node.id}")
|
||||
Wearable.getMessageClient(application).sendMessage(
|
||||
node.id,
|
||||
"/requestConfig",
|
||||
ByteArray(0)
|
||||
).apply {
|
||||
addOnSuccessListener {
|
||||
Log.d(TAG, "Request for config sent successfully")
|
||||
}
|
||||
addOnFailureListener { e ->
|
||||
Log.e(TAG, "Failed to request config", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
integrationUseCase.getEntities().forEach {
|
||||
entities[it.entityId] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
Wearable.getDataClient(getApplication<HomeAssistantApplication>()).removeListener(this)
|
||||
}
|
||||
|
||||
fun onEntitySelected(checked: Boolean, entityId: String) {
|
||||
if (checked)
|
||||
favoriteEntityIds.add(entityId)
|
||||
else
|
||||
favoriteEntityIds.remove(entityId)
|
||||
sendHomeFavorites(favoriteEntityIds.toList())
|
||||
}
|
||||
|
||||
fun onMove(fromItem: ItemPosition, toItem: ItemPosition) {
|
||||
favoriteEntityIds.move(
|
||||
favoriteEntityIds.indexOfFirst { it == fromItem.key },
|
||||
favoriteEntityIds.indexOfFirst { it == toItem.key }
|
||||
)
|
||||
}
|
||||
|
||||
fun canDragOver(position: ItemPosition) = favoriteEntityIds.any { it == position.key }
|
||||
|
||||
fun sendHomeFavorites(favoritesList: List<String>) = viewModelScope.launch {
|
||||
val application = getApplication<HomeAssistantApplication>()
|
||||
val putDataRequest = PutDataMapRequest.create("/updateFavorites").run {
|
||||
dataMap.putLong(KEY_UPDATE_TIME, System.nanoTime())
|
||||
dataMap.putString(KEY_FAVORITES, objectMapper.writeValueAsString(favoritesList))
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
||||
Wearable.getDataClient(application).putDataItem(putDataRequest).apply {
|
||||
addOnSuccessListener { Log.d(TAG, "Successfully sent favorites to wear") }
|
||||
addOnFailureListener { e ->
|
||||
Log.e(TAG, "Failed to send favorites to wear", e)
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.getString(commonR.string.failure_send_favorites_wear),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendAuthToWear(
|
||||
url: String,
|
||||
authCode: String,
|
||||
deviceName: String,
|
||||
deviceTrackingEnabled: Boolean
|
||||
) {
|
||||
val putDataRequest = PutDataMapRequest.create("/authenticate").run {
|
||||
dataMap.putString("URL", url)
|
||||
dataMap.putString("AuthCode", authCode)
|
||||
dataMap.putString("DeviceName", deviceName)
|
||||
dataMap.putBoolean("LocationTracking", deviceTrackingEnabled)
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
||||
Wearable.getDataClient(getApplication<HomeAssistantApplication>()).putDataItem(putDataRequest).apply {
|
||||
addOnSuccessListener { Log.d(TAG, "Successfully sent favorites to wear") }
|
||||
addOnFailureListener { e -> Log.e(TAG, "Failed to send favorites to wear", e) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDataChanged(dataEvents: DataEventBuffer) {
|
||||
Log.d(TAG, "onDataChanged ${dataEvents.count}")
|
||||
dataEvents.forEach { event ->
|
||||
if (event.type == DataEvent.TYPE_CHANGED) {
|
||||
event.dataItem.also { item ->
|
||||
when (item.uri.path) {
|
||||
"/config" -> {
|
||||
onLoadConfigFromWear(DataMapItem.fromDataItem(item).dataMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataEvents.release()
|
||||
}
|
||||
|
||||
private fun onLoadConfigFromWear(data: DataMap) {
|
||||
isAuthenticated.value = data.getBoolean(KEY_IS_AUTHENTICATED, false)
|
||||
val favoriteEntityIdList: List<String> =
|
||||
objectMapper.readValue(data.getString(KEY_FAVORITES, "[]"))
|
||||
favoriteEntityIds.clear()
|
||||
favoriteEntityIdList.forEach { entityId ->
|
||||
favoriteEntityIds.add(entityId)
|
||||
}
|
||||
hasData.value = true
|
||||
}
|
||||
}
|
|
@ -1,151 +1,151 @@
|
|||
package io.homeassistant.companion.android.settings.views
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import io.homeassistant.companion.android.settings.SettingsWearViewModel
|
||||
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
||||
import org.burnoutcrew.reorderable.draggedItem
|
||||
import org.burnoutcrew.reorderable.rememberReorderState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
const val WEAR_DOCS_LINK = "https://companion.home-assistant.io/docs/wear-os/wear-os"
|
||||
val supportedDomains = listOf(
|
||||
"input_boolean", "light", "lock", "switch", "script", "scene"
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LoadWearFavoritesSettings(
|
||||
settingsWearViewModel: SettingsWearViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val reorderState = rememberReorderState()
|
||||
|
||||
val validEntities = settingsWearViewModel.entities.filter { it.key.split(".")[0] in supportedDomains }.values.sortedBy { it.entityId }.toList()
|
||||
val favoriteEntities = settingsWearViewModel.favoriteEntityIds
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(commonR.string.wear_favorite_entities)) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(WEAR_DOCS_LINK))
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.HelpOutline,
|
||||
contentDescription = stringResource(id = commonR.string.help)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
LazyColumn(
|
||||
state = reorderState.listState,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 10.dp, start = 5.dp, end = 10.dp)
|
||||
.then(
|
||||
Modifier.reorderable(
|
||||
reorderState,
|
||||
{ from, to -> settingsWearViewModel.onMove(from, to) },
|
||||
canDragOver = { settingsWearViewModel.canDragOver(it) },
|
||||
onDragEnd = { _, _ ->
|
||||
settingsWearViewModel.sendHomeFavorites(settingsWearViewModel.favoriteEntityIds.toList())
|
||||
}
|
||||
)
|
||||
)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(commonR.string.wear_set_favorites),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
items(favoriteEntities.size, { favoriteEntities[it] }) { index ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.clickable {
|
||||
settingsWearViewModel.onEntitySelected(
|
||||
false,
|
||||
favoriteEntities[index]
|
||||
)
|
||||
}
|
||||
.draggedItem(
|
||||
reorderState.offsetByKey(favoriteEntities[index]),
|
||||
Orientation.Vertical
|
||||
)
|
||||
.detectReorderAfterLongPress(reorderState)
|
||||
) {
|
||||
val iconBitmap = IconicsDrawable(LocalContext.current, "cmd-drag_vertical").toBitmap().asImageBitmap()
|
||||
Icon(iconBitmap, "", modifier = Modifier.padding(top = 13.dp))
|
||||
Checkbox(
|
||||
checked = favoriteEntities.contains(favoriteEntities[index]),
|
||||
onCheckedChange = {
|
||||
settingsWearViewModel.onEntitySelected(it, favoriteEntities[index])
|
||||
},
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(
|
||||
text = favoriteEntities[index].replace("[", "").replace("]", ""),
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Divider()
|
||||
}
|
||||
if (!validEntities.isNullOrEmpty()) {
|
||||
items(validEntities.size) { index ->
|
||||
val item = validEntities[index]
|
||||
if (!favoriteEntities.contains(item.entityId)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(15.dp)
|
||||
.clickable {
|
||||
settingsWearViewModel.onEntitySelected(true, item.entityId)
|
||||
}
|
||||
) {
|
||||
Checkbox(
|
||||
checked = false,
|
||||
onCheckedChange = {
|
||||
settingsWearViewModel.onEntitySelected(it, item.entityId)
|
||||
},
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(
|
||||
text = item.entityId,
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package io.homeassistant.companion.android.settings.wear.views
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import io.homeassistant.companion.android.settings.wear.SettingsWearViewModel
|
||||
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
||||
import org.burnoutcrew.reorderable.draggedItem
|
||||
import org.burnoutcrew.reorderable.rememberReorderState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
const val WEAR_DOCS_LINK = "https://companion.home-assistant.io/docs/wear-os/wear-os"
|
||||
val supportedDomains = listOf(
|
||||
"input_boolean", "light", "lock", "switch", "script", "scene"
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LoadWearFavoritesSettings(
|
||||
settingsWearViewModel: SettingsWearViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val reorderState = rememberReorderState()
|
||||
|
||||
val validEntities = settingsWearViewModel.entities.filter { it.key.split(".")[0] in supportedDomains }.values.sortedBy { it.entityId }.toList()
|
||||
val favoriteEntities = settingsWearViewModel.favoriteEntityIds
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(commonR.string.wear_favorite_entities)) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(WEAR_DOCS_LINK))
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.HelpOutline,
|
||||
contentDescription = stringResource(id = commonR.string.help)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
LazyColumn(
|
||||
state = reorderState.listState,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 10.dp, start = 5.dp, end = 10.dp)
|
||||
.then(
|
||||
Modifier.reorderable(
|
||||
reorderState,
|
||||
{ from, to -> settingsWearViewModel.onMove(from, to) },
|
||||
canDragOver = { settingsWearViewModel.canDragOver(it) },
|
||||
onDragEnd = { _, _ ->
|
||||
settingsWearViewModel.sendHomeFavorites(settingsWearViewModel.favoriteEntityIds.toList())
|
||||
}
|
||||
)
|
||||
)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(commonR.string.wear_set_favorites),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
items(favoriteEntities.size, { favoriteEntities[it] }) { index ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.clickable {
|
||||
settingsWearViewModel.onEntitySelected(
|
||||
false,
|
||||
favoriteEntities[index]
|
||||
)
|
||||
}
|
||||
.draggedItem(
|
||||
reorderState.offsetByKey(favoriteEntities[index]),
|
||||
Orientation.Vertical
|
||||
)
|
||||
.detectReorderAfterLongPress(reorderState)
|
||||
) {
|
||||
val iconBitmap = IconicsDrawable(LocalContext.current, "cmd-drag_vertical").toBitmap().asImageBitmap()
|
||||
Icon(iconBitmap, "", modifier = Modifier.padding(top = 13.dp))
|
||||
Checkbox(
|
||||
checked = favoriteEntities.contains(favoriteEntities[index]),
|
||||
onCheckedChange = {
|
||||
settingsWearViewModel.onEntitySelected(it, favoriteEntities[index])
|
||||
},
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(
|
||||
text = favoriteEntities[index].replace("[", "").replace("]", ""),
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Divider()
|
||||
}
|
||||
if (!validEntities.isNullOrEmpty()) {
|
||||
items(validEntities.size) { index ->
|
||||
val item = validEntities[index]
|
||||
if (!favoriteEntities.contains(item.entityId)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(15.dp)
|
||||
.clickable {
|
||||
settingsWearViewModel.onEntitySelected(true, item.entityId)
|
||||
}
|
||||
) {
|
||||
Checkbox(
|
||||
checked = false,
|
||||
onCheckedChange = {
|
||||
settingsWearViewModel.onEntitySelected(it, item.entityId)
|
||||
},
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(
|
||||
text = item.entityId,
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +1,35 @@
|
|||
package io.homeassistant.companion.android.settings.views
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
import io.homeassistant.companion.android.settings.SettingsWearViewModel
|
||||
|
||||
@Composable
|
||||
fun LoadSettingsHomeView(
|
||||
settingsWearViewModel: SettingsWearViewModel,
|
||||
deviceName: String
|
||||
) {
|
||||
MdcTheme {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = SettingsWearMainView.LANDING) {
|
||||
composable(SettingsWearMainView.FAVORITES) {
|
||||
LoadWearFavoritesSettings(
|
||||
settingsWearViewModel
|
||||
)
|
||||
}
|
||||
composable(SettingsWearMainView.LANDING) {
|
||||
SettingWearLandingView(deviceName) {
|
||||
navController.navigate(SettingsWearMainView.FAVORITES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package io.homeassistant.companion.android.settings.wear.views
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
import io.homeassistant.companion.android.settings.wear.SettingsWearViewModel
|
||||
|
||||
@Composable
|
||||
fun LoadSettingsHomeView(
|
||||
settingsWearViewModel: SettingsWearViewModel,
|
||||
deviceName: String,
|
||||
loginWearOs: () -> Unit
|
||||
) {
|
||||
MdcTheme {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = SettingsWearMainView.LANDING) {
|
||||
composable(SettingsWearMainView.FAVORITES) {
|
||||
LoadWearFavoritesSettings(
|
||||
settingsWearViewModel
|
||||
)
|
||||
}
|
||||
composable(SettingsWearMainView.LANDING) {
|
||||
SettingWearLandingView(
|
||||
deviceName = deviceName,
|
||||
hasData = settingsWearViewModel.hasData.value,
|
||||
isAuthed = settingsWearViewModel.isAuthenticated.value,
|
||||
navigateFavorites = { navController.navigate(SettingsWearMainView.FAVORITES) },
|
||||
loginWearOs = loginWearOs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +1,106 @@
|
|||
package io.homeassistant.companion.android.settings.views
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.homeassistant.companion.android.util.wearDeviceName
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@Composable
|
||||
fun SettingWearLandingView(
|
||||
deviceName: String,
|
||||
navigateFavorites: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(commonR.string.wear_settings)) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(WEAR_DOCS_LINK)
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.HelpOutline,
|
||||
contentDescription = stringResource(id = commonR.string.help)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 20.dp, top = 10.dp, end = 20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = commonR.string.manage_favorites_device, deviceName),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Button(
|
||||
onClick = navigateFavorites,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp, end = 10.dp)
|
||||
) {
|
||||
Text(text = stringResource(commonR.string.set_favorites_on_device))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewSettingWearLandingView() {
|
||||
SettingWearLandingView(wearDeviceName) {}
|
||||
}
|
||||
package io.homeassistant.companion.android.settings.wear.views
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.homeassistant.companion.android.util.wearDeviceName
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@Composable
|
||||
fun SettingWearLandingView(
|
||||
deviceName: String,
|
||||
hasData: Boolean,
|
||||
isAuthed: Boolean,
|
||||
navigateFavorites: () -> Unit,
|
||||
loginWearOs: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(commonR.string.wear_settings)) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(WEAR_DOCS_LINK)
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.HelpOutline,
|
||||
contentDescription = stringResource(id = commonR.string.help)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 20.dp, top = 10.dp, end = 20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = commonR.string.manage_wear_device, deviceName),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
when {
|
||||
!hasData -> {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
}
|
||||
isAuthed -> {
|
||||
Button(
|
||||
onClick = navigateFavorites,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp, end = 10.dp)
|
||||
) {
|
||||
Text(stringResource(commonR.string.set_favorites_on_device))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Button(
|
||||
onClick = loginWearOs,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp, end = 10.dp)
|
||||
) {
|
||||
Text(stringResource(commonR.string.login_wear_os_device))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewSettingWearLandingView() {
|
||||
SettingWearLandingView(
|
||||
deviceName = wearDeviceName,
|
||||
hasData = true,
|
||||
isAuthed = true,
|
||||
navigateFavorites = {},
|
||||
loginWearOs = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package io.homeassistant.companion.android.settings.wear.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.gms.wearable.Node
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.onboarding.OnboardingActivity
|
||||
import io.homeassistant.companion.android.settings.wear.SettingsWearViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsWearMainView : AppCompatActivity() {
|
||||
|
||||
private val settingsWearViewModel by viewModels<SettingsWearViewModel>()
|
||||
|
||||
@Inject
|
||||
lateinit var integrationUseCase: IntegrationRepository
|
||||
|
||||
private val registerActivityResult =
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
this::onOnboardingComplete
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsWearDevice"
|
||||
private var currentNodes = setOf<Node>()
|
||||
const val LANDING = "Landing"
|
||||
const val FAVORITES = "Favorites"
|
||||
|
||||
fun newInstance(context: Context, wearNodes: Set<Node>): Intent {
|
||||
currentNodes = wearNodes
|
||||
return Intent(context, SettingsWearMainView::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
LoadSettingsHomeView(
|
||||
settingsWearViewModel,
|
||||
currentNodes.first().displayName,
|
||||
this::loginWearOs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loginWearOs() {
|
||||
registerActivityResult.launch(OnboardingActivity.newInstance(this, currentNodes.first().displayName, false))
|
||||
}
|
||||
|
||||
private fun onOnboardingComplete(result: ActivityResult) {
|
||||
val intent = result.data!!
|
||||
val url = intent.getStringExtra("URL").toString()
|
||||
val authCode = intent.getStringExtra("AuthCode").toString()
|
||||
val deviceName = intent.getStringExtra("DeviceName").toString()
|
||||
val deviceTrackingEnabled = intent.getBooleanExtra("LocationTracking", false)
|
||||
settingsWearViewModel.sendAuthToWear(url, authCode, deviceName, deviceTrackingEnabled)
|
||||
}
|
||||
}
|
|
@ -257,12 +257,12 @@
|
|||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsWearActivity"
|
||||
android:name=".settings.wear.SettingsWearActivity"
|
||||
android:parentActivityName=".settings.SettingsActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.views.SettingsWearMainView"
|
||||
android:name=".settings.wear.views.SettingsWearMainView"
|
||||
android:parentActivityName=".settings.SettingsActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
|
||||
|
|
|
@ -2,8 +2,10 @@ package io.homeassistant.companion.android.onboarding
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
|
@ -15,15 +17,24 @@ class OnboardingActivity : AppCompatActivity() {
|
|||
companion object {
|
||||
private const val AUTHENTICATION_FRAGMENT = "authentication_fragment"
|
||||
private const val TAG = "OnboardingActivity"
|
||||
private const val EXTRA_DEFAULT_DEVICE_NAME = "extra_default_device_name"
|
||||
private const val EXTRA_LOCATION_TRACKING_POSSIBLE = "location_tracking_possible"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, OnboardingActivity::class.java)
|
||||
fun newInstance(context: Context, defaultDeviceName: String = Build.MODEL, locationTrackingPossible: Boolean = true): Intent {
|
||||
return Intent(context, OnboardingActivity::class.java).apply {
|
||||
putExtra(EXTRA_DEFAULT_DEVICE_NAME, defaultDeviceName)
|
||||
putExtra(EXTRA_LOCATION_TRACKING_POSSIBLE, locationTrackingPossible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel by viewModels<OnboardingViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_onboarding)
|
||||
viewModel.deviceName.value = intent.getStringExtra(EXTRA_DEFAULT_DEVICE_NAME) ?: Build.MODEL
|
||||
viewModel.locationTrackingPossible.value = intent.getBooleanExtra(EXTRA_LOCATION_TRACKING_POSSIBLE, false)
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
|
|
|
@ -2,7 +2,6 @@ package io.homeassistant.companion.android.onboarding
|
|||
|
||||
import android.app.Application
|
||||
import android.net.nsd.NsdManager
|
||||
import android.os.Build
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
|
@ -38,7 +37,8 @@ class OnboardingViewModel @Inject constructor(
|
|||
val manualUrl = mutableStateOf("")
|
||||
val manualContinueEnabled = mutableStateOf(false)
|
||||
val authCode = mutableStateOf("")
|
||||
val deviceName = mutableStateOf(Build.MODEL)
|
||||
val deviceName = mutableStateOf("")
|
||||
val locationTrackingPossible = mutableStateOf(false)
|
||||
val locationTrackingEnabled = mutableStateOf(false)
|
||||
|
||||
fun onManualUrlUpdated(url: String) {
|
||||
|
|
|
@ -61,23 +61,25 @@ fun MobileAppIntegrationView(
|
|||
}
|
||||
)
|
||||
)
|
||||
Row {
|
||||
if (onboardingViewModel.locationTrackingPossible.value) {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(commonR.string.enable_location_tracking),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = onboardingViewModel.locationTrackingEnabled.value,
|
||||
onCheckedChange = onLocationTrackingChanged,
|
||||
colors = SwitchDefaults.colors(uncheckedThumbColor = MaterialTheme.colors.secondary)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(commonR.string.enable_location_tracking),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = onboardingViewModel.locationTrackingEnabled.value,
|
||||
onCheckedChange = onLocationTrackingChanged,
|
||||
colors = SwitchDefaults.colors(uncheckedThumbColor = MaterialTheme.colors.secondary)
|
||||
text = stringResource(id = commonR.string.enable_location_tracking_description),
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = commonR.string.enable_location_tracking_description),
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
TextButton(onClick = openPrivacyPolicy) {
|
||||
Text(stringResource(id = commonR.string.privacy_url))
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import io.homeassistant.companion.android.settings.qs.ManageTilesFragment
|
|||
import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsSettingsFragment
|
||||
import io.homeassistant.companion.android.settings.ssid.SsidDialogFragment
|
||||
import io.homeassistant.companion.android.settings.ssid.SsidPreference
|
||||
import io.homeassistant.companion.android.settings.wear.SettingsWearActivity
|
||||
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsSettingsFragment
|
||||
import io.homeassistant.companion.android.util.DisabledLocationHandler
|
||||
import io.homeassistant.companion.android.util.LocationPermissionInfoHandler
|
||||
|
|
|
@ -1,22 +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)
|
||||
}
|
||||
}
|
||||
}
|
||||
package io.homeassistant.companion.android.settings.wear
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +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.DataClient
|
||||
import com.google.android.gms.wearable.DataEventBuffer
|
||||
|
||||
class SettingsWearMainView : AppCompatActivity(), DataClient.OnDataChangedListener {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsWearMainView"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, SettingsWearMainView::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDataChanged(dataEvents: DataEventBuffer) {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
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.DataClient
|
||||
import com.google.android.gms.wearable.DataEventBuffer
|
||||
|
||||
class SettingsWearMainView : AppCompatActivity(), DataClient.OnDataChangedListener {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsWearMainView"
|
||||
|
||||
fun newInstance(context: Context): Intent {
|
||||
return Intent(context, SettingsWearMainView::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDataChanged(dataEvents: DataEventBuffer) {
|
||||
// No op
|
||||
}
|
||||
}
|
|
@ -244,6 +244,7 @@
|
|||
<string name="log">Log</string>
|
||||
<string name="logbook">Logbook</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_wear_os_device">Login Wear OS Device</string>
|
||||
<string name="logout">Logout</string>
|
||||
<string name="lovelace_view_dashboard">Lovelace View or Dashboard</string>
|
||||
<string name="lovelace">Lovelace</string>
|
||||
|
@ -251,7 +252,6 @@
|
|||
<string name="manage_all_notifications">Manage All Notifications</string>
|
||||
<string name="manage_all_sensors_summary">This device has %1$d available sensors to utilize.</string>
|
||||
<string name="manage_all_sensors">Manage All Sensors</string>
|
||||
<string name="manage_favorites_device">Manage Favorites on device %1$s</string>
|
||||
<string name="manage_shortcuts_summary">Manage launcher shortcuts</string>
|
||||
<string name="manage_shortcuts">Manage Shortcuts</string>
|
||||
<string name="manage_ssids_input_exists">This SSID already exists.</string>
|
||||
|
@ -260,6 +260,7 @@
|
|||
<string name="manage_this_notification">Manage This Notification</string>
|
||||
<string name="manage_tiles_summary">Setup and manage Quick Setting tiles here. They will not function until you set them up here.</string>
|
||||
<string name="manage_tiles">Manage Tiles</string>
|
||||
<string name="manage_wear_device">Manage device %1$s</string>
|
||||
<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>
|
||||
|
|
|
@ -99,6 +99,7 @@ dependencies {
|
|||
implementation("com.google.dagger:hilt-android:2.40.5")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.40.5")
|
||||
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.9.3")
|
||||
|
||||
implementation("com.mikepenz:iconics-core:5.3.3")
|
||||
|
|
|
@ -83,9 +83,11 @@
|
|||
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
|
||||
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
|
||||
<data android:scheme="wear" android:host="*"
|
||||
android:path="/send_home_favorites" />
|
||||
android:path="/authenticate" />
|
||||
<data android:scheme="wear" android:host="*"
|
||||
android:path="/save_home_favorites" />
|
||||
android:path="/requestConfig" />
|
||||
<data android:scheme="wear" android:host="*"
|
||||
android:path="/updateFavorites" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package io.homeassistant.companion.android.phone
|
||||
|
||||
import android.net.Uri
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.android.gms.wearable.DataClient
|
||||
import com.google.android.gms.wearable.DataEvent
|
||||
import com.google.android.gms.wearable.DataEventBuffer
|
||||
|
@ -12,38 +14,65 @@ import com.google.android.gms.wearable.PutDataMapRequest
|
|||
import com.google.android.gms.wearable.Wearable
|
||||
import com.google.android.gms.wearable.WearableListenerService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
|
||||
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.common.data.url.UrlRepository
|
||||
import io.homeassistant.companion.android.database.AppDatabase
|
||||
import io.homeassistant.companion.android.database.wear.Favorites
|
||||
import io.homeassistant.companion.android.home.HomeActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONArray
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChangedListener {
|
||||
|
||||
@Inject
|
||||
lateinit var authenticationRepository: AuthenticationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var urlRepository: UrlRepository
|
||||
|
||||
@Inject
|
||||
lateinit var integrationUseCase: IntegrationRepository
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PhoneSettingsListener"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "We have favorite message listener")
|
||||
private const val KEY_UPDATE_TIME = "UpdateTime"
|
||||
private const val KEY_IS_AUTHENTICATED = "isAuthenticated"
|
||||
private const val KEY_FAVORITES = "favorites"
|
||||
}
|
||||
|
||||
override fun onMessageReceived(event: MessageEvent) {
|
||||
Log.d(TAG, "Message received: $event")
|
||||
if (event.path == "/send_home_favorites") {
|
||||
val nodeId = event.sourceNodeId
|
||||
sendHomeFavorites(nodeId)
|
||||
if (event.path == "/requestConfig") {
|
||||
sendPhoneData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendPhoneData() = mainScope.launch {
|
||||
val currentFavorites =
|
||||
AppDatabase.getInstance(applicationContext).favoritesDao().getAll() ?: listOf()
|
||||
val putDataRequest = PutDataMapRequest.create("/config").run {
|
||||
dataMap.putLong(KEY_UPDATE_TIME, System.nanoTime())
|
||||
dataMap.putBoolean(KEY_IS_AUTHENTICATED, integrationUseCase.isRegistered())
|
||||
dataMap.putString(KEY_FAVORITES, objectMapper.writeValueAsString(currentFavorites.map { it.id }))
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
||||
Wearable.getDataClient(this@PhoneSettingsListener).putDataItem(putDataRequest).apply {
|
||||
addOnSuccessListener { Log.d(TAG, "Successfully sent /config to device") }
|
||||
addOnFailureListener { e -> Log.e(TAG, "Failed to send /config to device", e) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,71 +81,49 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
dataEvents.forEach { event ->
|
||||
if (event.type == DataEvent.TYPE_CHANGED) {
|
||||
event.dataItem.also { item ->
|
||||
if (item.uri.path?.compareTo("/save_home_favorites") == 0) {
|
||||
val data = getHomeFavorites(DataMapItem.fromDataItem(item).dataMap)
|
||||
Log.d(TAG, "onDataChanged: Received home favorites: $data")
|
||||
saveFavorites()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHomeFavorites(map: DataMap): String {
|
||||
map.apply {
|
||||
return getString("favorites", "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendHomeFavorites(nodeId: String) = mainScope.launch {
|
||||
Log.d(TAG, "sendHomeFavorites to: $nodeId")
|
||||
val currentFavorites = AppDatabase.getInstance(applicationContext).favoritesDao().getAll()
|
||||
val list = emptyList<String>().toMutableList()
|
||||
for (favorite in currentFavorites!!) {
|
||||
list += listOf(favorite.id)
|
||||
}
|
||||
val jsonArray = JSONArray(list.toString())
|
||||
val jsonString = List(jsonArray.length()) {
|
||||
jsonArray.getString(it)
|
||||
}.map { it }
|
||||
|
||||
Log.d(TAG, "new list: $jsonString")
|
||||
val putDataRequest = PutDataMapRequest.create("/home_favorites").run {
|
||||
dataMap.putString("favorites", jsonString.toString())
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
||||
Wearable.getDataClient(this@PhoneSettingsListener).putDataItem(putDataRequest).apply {
|
||||
addOnSuccessListener { Log.d(TAG, "Successfully sent favorites to device") }
|
||||
addOnFailureListener { e ->
|
||||
Log.e(TAG, "Failed to send favorites to device", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFavorites() {
|
||||
Log.d(TAG, "Finding existing favorites")
|
||||
val favoritesDao = AppDatabase.getInstance(applicationContext).favoritesDao()
|
||||
mainScope.launch {
|
||||
Wearable.getDataClient(applicationContext).getDataItems(Uri.parse("wear://*/save_home_favorites"))
|
||||
.addOnSuccessListener {
|
||||
Log.d(TAG, "Found existing favorites: ${it.count}")
|
||||
it.forEach { dataItem ->
|
||||
val data = getHomeFavorites(DataMapItem.fromDataItem(dataItem).dataMap).removeSurrounding("[", "]").split(", ").toList()
|
||||
Log.d(
|
||||
TAG,
|
||||
"Favorites: $data"
|
||||
)
|
||||
favoritesDao.deleteAll()
|
||||
if (data.isNotEmpty()) {
|
||||
data.forEachIndexed { index, s ->
|
||||
favoritesDao.add(Favorites(s, index))
|
||||
}
|
||||
when (item.uri.path) {
|
||||
"/authenticate" -> {
|
||||
login(DataMapItem.fromDataItem(item).dataMap)
|
||||
}
|
||||
"/updateFavorites" -> {
|
||||
saveFavorites(DataMapItem.fromDataItem(item).dataMap)
|
||||
}
|
||||
}
|
||||
it.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
dataEvents.release()
|
||||
}
|
||||
|
||||
private fun login(dataMap: DataMap) = mainScope.launch {
|
||||
val url = dataMap.getString("URL")
|
||||
val authCode = dataMap.getString("AuthCode")
|
||||
val deviceName = dataMap.getString("DeviceName")
|
||||
val deviceTrackingEnabled = dataMap.getString("LocationTracking")
|
||||
|
||||
urlRepository.saveUrl(url)
|
||||
authenticationRepository.registerAuthorizationCode(authCode)
|
||||
integrationUseCase.registerDevice(
|
||||
DeviceRegistration(
|
||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||
deviceName
|
||||
)
|
||||
)
|
||||
|
||||
sendPhoneData()
|
||||
|
||||
val intent = HomeActivity.newInstance(applicationContext)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun saveFavorites(dataMap: DataMap) {
|
||||
val favoritesIds: List<String> =
|
||||
objectMapper.readValue(dataMap.getString(KEY_FAVORITES, "[]"))
|
||||
val favoritesDao = AppDatabase.getInstance(applicationContext).favoritesDao()
|
||||
favoritesDao.deleteAll()
|
||||
favoritesIds.forEachIndexed { index, entityId ->
|
||||
favoritesDao.add(Favorites(entityId, index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
<string-array name="android_wear_capabilities">
|
||||
<item>authentication_token</item>
|
||||
<item>verify_wear_app</item>
|
||||
<item>send_home_favorites</item>
|
||||
<item>save_home_favorites</item>
|
||||
<item>sends_config</item>
|
||||
</string-array>
|
||||
</resources>
|
Loading…
Reference in a new issue