mirror of
https://github.com/home-assistant/android
synced 2024-10-15 12:32:54 +00:00
Sync Thread datasets when starting Matter commissioning (#3349)
* Sync Thread datasets when starting Matter commissioning - When starting Matter commissioning from the frontend, check if there are Thread datasets on the device or in core but not the other, and if so try to sync them * Also sync Thread datasets in shared device flow - Datasets may not be used by the system but it is useful for future devices - Make methods in ThreadManager a bit more general
This commit is contained in:
parent
5f164ff5a6
commit
25dd869272
|
@ -168,6 +168,7 @@ dependencies {
|
|||
|
||||
"fullImplementation"("com.google.android.gms:play-services-location:21.0.1")
|
||||
"fullImplementation"("com.google.android.gms:play-services-home:16.0.0")
|
||||
"fullImplementation"("com.google.android.gms:play-services-threadnetwork:16.0.0-beta02")
|
||||
"fullImplementation"(platform("com.google.firebase:firebase-bom:31.2.2"))
|
||||
"fullImplementation"("com.google.firebase:firebase-messaging")
|
||||
"fullImplementation"("io.sentry:sentry-android:6.14.0")
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.google.android.gms.threadnetwork" />
|
||||
|
||||
<application
|
||||
android:name="io.homeassistant.companion.android.HomeAssistantApplication" >
|
||||
|
|
|
@ -3,11 +3,14 @@ package io.homeassistant.companion.android.matter
|
|||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import com.google.android.gms.home.matter.Matter
|
||||
import com.google.android.gms.home.matter.commissioning.SharedDeviceData
|
||||
|
@ -16,6 +19,7 @@ import io.homeassistant.companion.android.common.data.servers.ServerManager
|
|||
import io.homeassistant.companion.android.database.server.Server
|
||||
import io.homeassistant.companion.android.matter.views.MatterCommissioningView
|
||||
import io.homeassistant.companion.android.webview.WebViewActivity
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -33,6 +37,10 @@ class MatterCommissioningActivity : AppCompatActivity() {
|
|||
private var deviceName by mutableStateOf<String?>(null)
|
||||
private var servers by mutableStateOf<List<Server>>(emptyList())
|
||||
|
||||
private val threadPermissionLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
|
||||
deviceCode?.let { viewModel.onThreadPermissionResult(result, it) }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -43,7 +51,7 @@ class MatterCommissioningActivity : AppCompatActivity() {
|
|||
deviceName = deviceName,
|
||||
servers = servers,
|
||||
onSelectServer = viewModel::checkSupport,
|
||||
onConfirmCommissioning = { deviceCode?.let { viewModel.commissionDeviceWithCode(it) } },
|
||||
onConfirmCommissioning = { startCommissioning() },
|
||||
onClose = { finish() },
|
||||
onContinue = { continueToApp(false) }
|
||||
)
|
||||
|
@ -80,6 +88,19 @@ class MatterCommissioningActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun startCommissioning() {
|
||||
lifecycleScope.launch {
|
||||
val threadIntent = viewModel.syncThreadIfNecessary()
|
||||
if (threadIntent != null) {
|
||||
threadPermissionLauncher.launch(IntentSenderRequest.Builder(threadIntent).build())
|
||||
} else {
|
||||
deviceCode?.let {
|
||||
viewModel.commissionDeviceWithCode(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueToApp(hideTransition: Boolean) {
|
||||
startActivity(WebViewActivity.newInstance(this, null, viewModel.serverId))
|
||||
finish()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package io.homeassistant.companion.android.matter
|
||||
|
||||
import android.app.Application
|
||||
import android.content.IntentSender
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
@ -8,12 +10,14 @@ import androidx.lifecycle.AndroidViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.thread.ThreadManager
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MatterCommissioningViewModel @Inject constructor(
|
||||
private val matterManager: MatterManager,
|
||||
private val threadManager: ThreadManager,
|
||||
private val serverManager: ServerManager,
|
||||
application: Application
|
||||
) : AndroidViewModel(application) {
|
||||
|
@ -75,6 +79,22 @@ class MatterCommissioningViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun syncThreadIfNecessary(): IntentSender? {
|
||||
step = CommissioningFlowStep.Working
|
||||
return threadManager.syncPreferredDataset(
|
||||
getApplication<Application>().applicationContext,
|
||||
serverId,
|
||||
viewModelScope
|
||||
)
|
||||
}
|
||||
|
||||
fun onThreadPermissionResult(result: ActivityResult, code: String) {
|
||||
viewModelScope.launch {
|
||||
threadManager.sendThreadDatasetExportResult(result, serverId)
|
||||
commissionDeviceWithCode(code)
|
||||
}
|
||||
}
|
||||
|
||||
fun commissionDeviceWithCode(code: String) {
|
||||
viewModelScope.launch {
|
||||
step = CommissioningFlowStep.Working
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package io.homeassistant.companion.android.thread
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import com.google.android.gms.threadnetwork.ThreadBorderAgent
|
||||
import com.google.android.gms.threadnetwork.ThreadNetwork
|
||||
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
|
||||
import io.homeassistant.companion.android.common.data.HomeAssistantVersion
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class ThreadManagerImpl @Inject constructor(
|
||||
private val serverManager: ServerManager
|
||||
) : ThreadManager {
|
||||
companion object {
|
||||
private const val TAG = "ThreadManagerImpl"
|
||||
|
||||
// ID is a placeholder while we wait for Google to remove the requirement to provide one
|
||||
private const val BORDER_AGENT_ID = "0000000000000001"
|
||||
}
|
||||
|
||||
override fun appSupportsThread(): Boolean =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
|
||||
|
||||
override suspend fun coreSupportsThread(serverId: Int): Boolean {
|
||||
if (!serverManager.isRegistered() || serverManager.getServer(serverId) == null) return false
|
||||
val config = serverManager.webSocketRepository(serverId).getConfig()
|
||||
return config != null &&
|
||||
config.components.contains("thread") &&
|
||||
HomeAssistantVersion.fromString(config.version)?.isAtLeast(2023, 3, 0) == true
|
||||
}
|
||||
|
||||
override suspend fun syncPreferredDataset(
|
||||
context: Context,
|
||||
serverId: Int,
|
||||
scope: CoroutineScope
|
||||
): IntentSender? {
|
||||
if (!appSupportsThread() || !coreSupportsThread(serverId)) return null
|
||||
|
||||
val getDeviceDataset = scope.async { getPreferredDatasetFromDevice(context) }
|
||||
val getCoreDataset = scope.async { getPreferredDatasetFromServer(serverId) }
|
||||
val deviceThreadIntent = getDeviceDataset.await()
|
||||
val coreThreadDataset = getCoreDataset.await()
|
||||
|
||||
if (deviceThreadIntent == null && coreThreadDataset != null) {
|
||||
try {
|
||||
importDatasetFromServer(context, coreThreadDataset.datasetId, serverId)
|
||||
Log.d(TAG, "Thread import to device completed")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Thread import to device failed", e)
|
||||
}
|
||||
} else if (deviceThreadIntent != null && coreThreadDataset == null) {
|
||||
Log.d(TAG, "Thread export is ready")
|
||||
return deviceThreadIntent
|
||||
} // else if device and core both have or don't have datasets, continue
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun getPreferredDatasetFromServer(serverId: Int): ThreadDatasetResponse? {
|
||||
val datasets = serverManager.webSocketRepository(serverId).getThreadDatasets()
|
||||
return datasets?.firstOrNull { it.preferred }
|
||||
}
|
||||
|
||||
override suspend fun importDatasetFromServer(context: Context, datasetId: String, serverId: Int) {
|
||||
val tlv = serverManager.webSocketRepository(serverId).getThreadDatasetTlv(datasetId)?.tlvAsByteArray
|
||||
if (tlv != null) {
|
||||
val threadBorderAgent = ThreadBorderAgent.newBuilder(BORDER_AGENT_ID.toByteArray()).build()
|
||||
val threadNetworkCredentials = ThreadNetworkCredentials.fromActiveOperationalDataset(tlv)
|
||||
suspendCoroutine { cont ->
|
||||
ThreadNetwork.getClient(context).addCredentials(threadBorderAgent, threadNetworkCredentials)
|
||||
.addOnSuccessListener { cont.resume(Unit) }
|
||||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPreferredDatasetFromDevice(context: Context): IntentSender? = suspendCoroutine { cont ->
|
||||
if (appSupportsThread()) {
|
||||
ThreadNetwork.getClient(context)
|
||||
.preferredCredentials
|
||||
.addOnSuccessListener { cont.resume(it.intentSender) }
|
||||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
} else {
|
||||
cont.resumeWithException(IllegalStateException("Thread is not supported on SDK <27"))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendThreadDatasetExportResult(result: ActivityResult, serverId: Int) {
|
||||
if (result.resultCode == Activity.RESULT_OK && coreSupportsThread(serverId)) {
|
||||
val threadNetworkCredentials = ThreadNetworkCredentials.fromIntentSenderResultData(result.data!!)
|
||||
try {
|
||||
serverManager.webSocketRepository(serverId).addThreadDataset(threadNetworkCredentials.activeOperationalDataset)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error while executing server new Thread credentials request", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package io.homeassistant.companion.android.matter
|
|||
enum class MatterFrontendCommissioningStatus {
|
||||
NOT_STARTED,
|
||||
REQUESTED,
|
||||
THREAD_EXPORT_TO_SERVER,
|
||||
IN_PROGRESS,
|
||||
ERROR
|
||||
}
|
||||
|
|
|
@ -29,13 +29,13 @@ interface MatterManager {
|
|||
|
||||
/**
|
||||
* Send a request to the server to add a Matter device to the network and commission it
|
||||
* @return `true` if the request was successful
|
||||
* @return [MatterCommissionResponse], or `null` if it wasn't possible to complete the request
|
||||
*/
|
||||
suspend fun commissionDevice(code: String, serverId: Int): MatterCommissionResponse?
|
||||
|
||||
/**
|
||||
* Send a request to the server to commission an "on network" Matter device
|
||||
* @return `true` if the request was successful
|
||||
* @return [MatterCommissionResponse], or `null` if it wasn't possible to complete the request
|
||||
*/
|
||||
suspend fun commissionOnNetworkDevice(pin: Long, serverId: Int): MatterCommissionResponse?
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package io.homeassistant.companion.android.thread
|
||||
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import androidx.activity.result.ActivityResult
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface ThreadManager {
|
||||
|
||||
/**
|
||||
* Indicates if the app on this device supports Thread credential management.
|
||||
*/
|
||||
fun appSupportsThread(): Boolean
|
||||
|
||||
/**
|
||||
* Indicates if the server supports Thread credential management.
|
||||
*/
|
||||
suspend fun coreSupportsThread(serverId: Int): Boolean
|
||||
|
||||
/**
|
||||
* Try to sync the preferred Thread dataset with the device and server. If one has a preferred
|
||||
* dataset while the other one doesn't, it will sync. If both have or don't have preferred
|
||||
* datasets, skip syncing.
|
||||
* @return [IntentSender] if permission is required to import the device dataset, `null` if
|
||||
* syncing completed or there is nothing to sync
|
||||
*/
|
||||
suspend fun syncPreferredDataset(
|
||||
context: Context,
|
||||
serverId: Int,
|
||||
scope: CoroutineScope
|
||||
): IntentSender?
|
||||
|
||||
/**
|
||||
* Get the preferred Thread dataset from the server.
|
||||
*/
|
||||
suspend fun getPreferredDatasetFromServer(serverId: Int): ThreadDatasetResponse?
|
||||
|
||||
/**
|
||||
* Import a Thread dataset from the server to this device.
|
||||
* @param datasetId The dataset ID as provided by the server
|
||||
* @throws Exception if a preferred dataset exists on the server, but it wasn't possible to
|
||||
* import it
|
||||
*/
|
||||
suspend fun importDatasetFromServer(context: Context, datasetId: String, serverId: Int)
|
||||
|
||||
/**
|
||||
* Start a flow to get the preferred Thread dataset from this device to export to the server.
|
||||
* @return [IntentSender] to ask the user for permission to share the preferred dataset, or
|
||||
* `null` if there are no datasets to import
|
||||
* @throws Exception if it is not possible to get the preferred dataset
|
||||
*/
|
||||
suspend fun getPreferredDatasetFromDevice(context: Context): IntentSender?
|
||||
|
||||
/**
|
||||
* Process the result from [syncPreferredDataset] or [getPreferredDatasetFromDevice]'s intent
|
||||
* and add the Thread dataset, if any, to the server.
|
||||
*/
|
||||
suspend fun sendThreadDatasetExportResult(result: ActivityResult, serverId: Int)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.homeassistant.companion.android.thread
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class ThreadModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindThreadManager(threadManager: ThreadManagerImpl): ThreadManager
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package io.homeassistant.companion.android.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -164,9 +163,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
|
|||
mFilePathCallback = null
|
||||
}
|
||||
private val commissionMatterDevice = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
|
||||
// Any errors will have been shown in the UI provided by Play Services
|
||||
if (result.resultCode == Activity.RESULT_OK) Log.d(TAG, "Matter commissioning returned success")
|
||||
else Log.d(TAG, "Matter commissioning returned with non-OK code ${result.resultCode}")
|
||||
presenter.onMatterCommissioningIntentResult(this, result)
|
||||
}
|
||||
|
||||
@Inject
|
||||
|
@ -683,6 +680,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
|
|||
presenter.getMatterCommissioningStatusFlow().collect {
|
||||
Log.d(TAG, "Matter commissioning status changed to $it")
|
||||
when (it) {
|
||||
MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER,
|
||||
MatterFrontendCommissioningStatus.IN_PROGRESS -> {
|
||||
presenter.getMatterCommissioningIntent()?.let { intentSender ->
|
||||
commissionMatterDevice.launch(IntentSenderRequest.Builder(intentSender).build())
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.homeassistant.companion.android.webview
|
|||
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import androidx.activity.result.ActivityResult
|
||||
import io.homeassistant.companion.android.matter.MatterFrontendCommissioningStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
@ -47,5 +48,6 @@ interface WebViewPresenter {
|
|||
fun startCommissioningMatterDevice(context: Context)
|
||||
fun getMatterCommissioningStatusFlow(): Flow<MatterFrontendCommissioningStatus>
|
||||
fun getMatterCommissioningIntent(): IntentSender?
|
||||
fun onMatterCommissioningIntentResult(context: Context, result: ActivityResult)
|
||||
fun confirmMatterCommissioningError()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package io.homeassistant.companion.android.webview
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import dagger.hilt.android.qualifiers.ActivityContext
|
||||
import io.homeassistant.companion.android.common.data.authentication.SessionState
|
||||
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
|
||||
|
@ -12,6 +14,7 @@ import io.homeassistant.companion.android.common.data.servers.ServerManager
|
|||
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
|
||||
import io.homeassistant.companion.android.matter.MatterFrontendCommissioningStatus
|
||||
import io.homeassistant.companion.android.matter.MatterManager
|
||||
import io.homeassistant.companion.android.thread.ThreadManager
|
||||
import io.homeassistant.companion.android.util.UrlHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -36,7 +39,8 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
@ActivityContext context: Context,
|
||||
private val serverManager: ServerManager,
|
||||
private val prefsRepository: PrefsRepository,
|
||||
private val matterUseCase: MatterManager
|
||||
private val matterUseCase: MatterManager,
|
||||
private val threadUseCase: ThreadManager
|
||||
) : WebViewPresenter {
|
||||
|
||||
companion object {
|
||||
|
@ -289,6 +293,19 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
if (_matterCommissioningStatus.value != MatterFrontendCommissioningStatus.REQUESTED) {
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.REQUESTED)
|
||||
|
||||
mainScope.launch {
|
||||
val deviceThreadIntent = threadUseCase.syncPreferredDataset(context, serverId, this)
|
||||
if (deviceThreadIntent != null) {
|
||||
matterCommissioningIntentSender = deviceThreadIntent
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER)
|
||||
} else {
|
||||
startMatterCommissioningFlow(context)
|
||||
}
|
||||
}
|
||||
} // else already waiting for a result, don't send another request
|
||||
}
|
||||
|
||||
private fun startMatterCommissioningFlow(context: Context) {
|
||||
matterUseCase.startNewCommissioningFlow(
|
||||
context,
|
||||
{ intentSender ->
|
||||
|
@ -301,7 +318,6 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.ERROR)
|
||||
}
|
||||
)
|
||||
} // else already waiting for a result, don't send another request
|
||||
}
|
||||
|
||||
override fun getMatterCommissioningStatusFlow(): Flow<MatterFrontendCommissioningStatus> =
|
||||
|
@ -313,6 +329,22 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
return intent
|
||||
}
|
||||
|
||||
override fun onMatterCommissioningIntentResult(context: Context, result: ActivityResult) {
|
||||
when (_matterCommissioningStatus.value) {
|
||||
MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER -> {
|
||||
mainScope.launch {
|
||||
threadUseCase.sendThreadDatasetExportResult(result, serverId)
|
||||
startMatterCommissioningFlow(context)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Any errors will have been shown in the UI provided by Play Services
|
||||
if (result.resultCode == Activity.RESULT_OK) Log.d(TAG, "Matter commissioning returned success")
|
||||
else Log.d(TAG, "Matter commissioning returned with non-OK code ${result.resultCode}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun confirmMatterCommissioningError() {
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.NOT_STARTED)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package io.homeassistant.companion.android.thread
|
||||
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import androidx.activity.result.ActivityResult
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Inject
|
||||
|
||||
class ThreadManagerImpl @Inject constructor() : ThreadManager {
|
||||
|
||||
// Thread support currently depends on Google Play Services,
|
||||
// and as a result Thread is not supported with the minimal flavor
|
||||
|
||||
override fun appSupportsThread(): Boolean = false
|
||||
|
||||
override suspend fun coreSupportsThread(serverId: Int): Boolean = false
|
||||
|
||||
override suspend fun syncPreferredDataset(
|
||||
context: Context,
|
||||
serverId: Int,
|
||||
scope: CoroutineScope
|
||||
): IntentSender? = null
|
||||
|
||||
override suspend fun getPreferredDatasetFromServer(serverId: Int): ThreadDatasetResponse? = null
|
||||
|
||||
override suspend fun importDatasetFromServer(context: Context, datasetId: String, serverId: Int) { }
|
||||
|
||||
override suspend fun getPreferredDatasetFromDevice(context: Context): IntentSender? {
|
||||
throw IllegalStateException("Thread is not supported with the minimal flavor")
|
||||
}
|
||||
|
||||
override suspend fun sendThreadDatasetExportResult(result: ActivityResult, serverId: Int) { }
|
||||
}
|
|
@ -16,6 +16,8 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ge
|
|||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.MatterCommissionResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.StateChangedEvent
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetTlvResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TriggerEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
@ -41,6 +43,9 @@ interface WebSocketRepository {
|
|||
suspend fun ackNotification(confirmId: String): Boolean
|
||||
suspend fun commissionMatterDevice(code: String): MatterCommissionResponse?
|
||||
suspend fun commissionMatterDeviceOnNetwork(pin: Long): MatterCommissionResponse?
|
||||
suspend fun getThreadDatasets(): List<ThreadDatasetResponse>?
|
||||
suspend fun getThreadDatasetTlv(datasetId: String): ThreadDatasetTlvResponse?
|
||||
suspend fun addThreadDataset(tlv: ByteArray): Boolean
|
||||
suspend fun getConversation(speech: String): ConversationResponse?
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,10 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ma
|
|||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.SocketResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.StateChangedEvent
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetTlvResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TriggerEvent
|
||||
import io.homeassistant.companion.android.common.util.toHexString
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -394,6 +397,52 @@ class WebSocketRepositoryImpl @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all Thread datasets known to the server.
|
||||
* @return List with [ThreadDatasetResponse]s, or `null` if not an admin or no response.
|
||||
*/
|
||||
override suspend fun getThreadDatasets(): List<ThreadDatasetResponse>? {
|
||||
val response = sendMessage(
|
||||
mapOf(
|
||||
"type" to "thread/list_datasets"
|
||||
)
|
||||
)
|
||||
return if (response?.success == true && response.result?.contains("datasets") == true) {
|
||||
mapper.convertValue(response.result["datasets"]!!)
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the TLV value for a dataset.
|
||||
* @return [ThreadDatasetTlvResponse] for the Thread dataset, or `null` if not found, not an
|
||||
* admin or no response.
|
||||
*/
|
||||
override suspend fun getThreadDatasetTlv(datasetId: String): ThreadDatasetTlvResponse? {
|
||||
val response = sendMessage(
|
||||
mapOf(
|
||||
"type" to "thread/get_dataset_tlv",
|
||||
"dataset_id" to datasetId
|
||||
)
|
||||
)
|
||||
|
||||
return mapResponse(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new set of Thread network credentials to the server.
|
||||
* @return `true` if the server indicated success
|
||||
*/
|
||||
override suspend fun addThreadDataset(tlv: ByteArray): Boolean {
|
||||
val response = sendMessage(
|
||||
mapOf(
|
||||
"type" to "thread/add_dataset_tlv",
|
||||
"source" to "Google",
|
||||
"tlv" to tlv.toHexString()
|
||||
)
|
||||
)
|
||||
return response?.success == true
|
||||
}
|
||||
|
||||
private suspend fun connect(): Boolean {
|
||||
connectedMutex.withLock {
|
||||
if (connection != null && connected.isCompleted) {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package io.homeassistant.companion.android.common.data.websocket.impl.entities
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ThreadDatasetResponse(
|
||||
val datasetId: String,
|
||||
val extendedPanId: String,
|
||||
val networkName: String,
|
||||
val panId: String,
|
||||
val preferred: Boolean,
|
||||
val source: String
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package io.homeassistant.companion.android.common.data.websocket.impl.entities
|
||||
|
||||
data class ThreadDatasetTlvResponse(
|
||||
val tlv: String
|
||||
) {
|
||||
val tlvAsByteArray: ByteArray
|
||||
get() = tlv.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.homeassistant.companion.android.common.util
|
||||
|
||||
import okhttp3.internal.and
|
||||
|
||||
private val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
|
||||
|
||||
fun ByteArray.toHexString(): String { // From https://stackoverflow.com/a/9855338/4214819
|
||||
val hexChars = CharArray(this.size * 2)
|
||||
for (j in 0 until this.size) {
|
||||
val v = get(j) and 0xFF
|
||||
hexChars[j * 2] = HEX_ARRAY[v ushr 4]
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
|
||||
}
|
||||
return String(hexChars)
|
||||
}
|
Loading…
Reference in a new issue