Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support for Thread "import credentials" from frontend #4128

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class MatterCommissioningViewModel @Inject constructor(
val result = threadManager.syncPreferredDataset(
getApplication<Application>().applicationContext,
serverId,
false,
viewModelScope
)
when (result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResult
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.threadnetwork.IsPreferredCredentialsResult
import com.google.android.gms.threadnetwork.ThreadBorderAgent
import com.google.android.gms.threadnetwork.ThreadNetwork
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
import com.google.android.gms.threadnetwork.ThreadNetworkStatusCodes
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
Expand Down Expand Up @@ -49,11 +51,52 @@ class ThreadManagerImpl @Inject constructor(
override suspend fun syncPreferredDataset(
context: Context,
serverId: Int,
exportOnly: Boolean,
scope: CoroutineScope
): ThreadManager.SyncResult {
if (!appSupportsThread()) return ThreadManager.SyncResult.AppUnsupported
if (!coreSupportsThread(serverId)) return ThreadManager.SyncResult.ServerUnsupported

return if (exportOnly) { // Limited sync, only export non-app dataset
exportSyncPreferredDataset(context)
} else { // Full sync
fullSyncPreferredDataset(context, serverId, scope)
}
}

private suspend fun exportSyncPreferredDataset(
context: Context
): ThreadManager.SyncResult {
val getDeviceDataset = try {
getPreferredDatasetFromDevice(context)
} catch (e: ApiException) {
Log.e(TAG, "Thread: export cannot be started", e)
if (e.statusCode == ThreadNetworkStatusCodes.LOCAL_NETWORK_NOT_CONNECTED) {
return ThreadManager.SyncResult.NotConnected
} else {
throw e
}
}

return if (getDeviceDataset == null) {
ThreadManager.SyncResult.NoneHaveCredentials
} else {
val appIsDevicePreferred = appAddedIsPreferredCredentials(context)
Log.d(TAG, "Thread: device ${if (appIsDevicePreferred) "prefers" else "doesn't prefer" } dataset from app")

return if (appIsDevicePreferred) {
ThreadManager.SyncResult.OnlyOnServer(imported = false)
} else {
ThreadManager.SyncResult.OnlyOnDevice(exportIntent = getDeviceDataset)
}
}
}

private suspend fun fullSyncPreferredDataset(
context: Context,
serverId: Int,
scope: CoroutineScope
): ThreadManager.SyncResult {
deleteOrphanedThreadCredentials(context, serverId)

val getDeviceDataset = scope.async { getPreferredDatasetFromDevice(context) }
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class DeveloperSettingsPresenterImpl @Inject constructor(
override fun runThreadDebug(context: Context, serverId: Int) {
mainScope.launch {
try {
when (val syncResult = threadManager.syncPreferredDataset(context, serverId, CoroutineScope(coroutineContext + SupervisorJob()))) {
when (val syncResult = threadManager.syncPreferredDataset(context, serverId, false, CoroutineScope(coroutineContext + SupervisorJob()))) {
is ThreadManager.SyncResult.ServerUnsupported ->
view.onThreadDebugResult(context.getString(commonR.string.thread_debug_result_unsupported_server), false)
is ThreadManager.SyncResult.OnlyOnServer -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ThreadManager {
sealed class SyncResult {
object AppUnsupported : SyncResult()
object ServerUnsupported : SyncResult()
object NotConnected : SyncResult()
class OnlyOnServer(val imported: Boolean) : SyncResult()
class OnlyOnDevice(val exportIntent: IntentSender?) : SyncResult()
class AllHaveCredentials(val matches: Boolean?, val fromApp: Boolean?, val updated: Boolean?, val exportIntent: IntentSender?) : SyncResult()
Expand All @@ -28,15 +29,21 @@ interface ThreadManager {
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 preferred datasets, it will
* send updated data to the server if needed. If neither has a preferred dataset, skip syncing.
* Try to sync the preferred Thread dataset.
* @param exportOnly Controls the synchronization direction.
* - If set to `true`, only get the device preferred dataset and sync to the server if it
* wasn't added by the app.
* - If set to `false`, try to get the device and server in sync. This will clean up old/stale
* app datasets. If one has a preferred dataset while the other one doesn't, it will sync to
* the other. If both have preferred datasets, it will send updated data to the server if
* needed. If neither has a preferred dataset, skip syncing.
* @return [SyncResult] with details of the sync operation, which may include an [IntentSender]
* if permission is required to import the device dataset
*/
suspend fun syncPreferredDataset(
context: Context,
serverId: Int,
exportOnly: Boolean,
scope: CoroutineScope
): SyncResult

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.homeassistant.companion.android.webview

enum class MatterThreadStep {
NOT_STARTED,
REQUESTED,
THREAD_EXPORT_TO_SERVER_MATTER,
THREAD_EXPORT_TO_SERVER_ONLY,
MATTER_IN_PROGRESS,
THREAD_SENT,
THREAD_NONE,
ERROR_MATTER,
ERROR_THREAD_LOCAL_NETWORK,
ERROR_THREAD_OTHER
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ import io.homeassistant.companion.android.database.authentication.Authentication
import io.homeassistant.companion.android.databinding.ActivityWebviewBinding
import io.homeassistant.companion.android.databinding.DialogAuthenticationBinding
import io.homeassistant.companion.android.launch.LaunchActivity
import io.homeassistant.companion.android.matter.MatterFrontendCommissioningStatus
import io.homeassistant.companion.android.nfc.WriteNfcTag
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.sensors.SensorWorker
Expand Down Expand Up @@ -171,7 +170,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
mFilePathCallback = null
}
private val commissionMatterDevice = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
presenter.onMatterCommissioningIntentResult(this, result)
presenter.onMatterThreadIntentResult(this, result)
}

@Inject
Expand Down Expand Up @@ -631,18 +630,45 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi

lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
presenter.getMatterCommissioningStatusFlow().collect {
Log.d(TAG, "Matter commissioning status changed to $it")
presenter.getMatterThreadStepFlow().collect {
Log.d(TAG, "Matter/Thread step changed to $it")
when (it) {
MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER,
MatterFrontendCommissioningStatus.IN_PROGRESS -> {
presenter.getMatterCommissioningIntent()?.let { intentSender ->
MatterThreadStep.THREAD_EXPORT_TO_SERVER_MATTER,
MatterThreadStep.THREAD_EXPORT_TO_SERVER_ONLY,
MatterThreadStep.MATTER_IN_PROGRESS -> {
presenter.getMatterThreadIntent()?.let { intentSender ->
commissionMatterDevice.launch(IntentSenderRequest.Builder(intentSender).build())
}
}
MatterFrontendCommissioningStatus.ERROR -> {
MatterThreadStep.THREAD_NONE -> {
alertDialog?.cancel()
AlertDialog.Builder(this@WebViewActivity)
.setMessage(commonR.string.thread_export_none)
.setPositiveButton(commonR.string.ok, null)
.show()
presenter.finishMatterThreadFlow()
}
MatterThreadStep.THREAD_SENT -> {
Toast.makeText(this@WebViewActivity, commonR.string.thread_export_success, Toast.LENGTH_SHORT).show()
alertDialog?.cancel()
presenter.finishMatterThreadFlow()
}
MatterThreadStep.ERROR_MATTER -> {
Toast.makeText(this@WebViewActivity, commonR.string.matter_commissioning_unavailable, Toast.LENGTH_SHORT).show()
presenter.confirmMatterCommissioningError()
presenter.finishMatterThreadFlow()
}
MatterThreadStep.ERROR_THREAD_LOCAL_NETWORK -> {
alertDialog?.cancel()
AlertDialog.Builder(this@WebViewActivity)
.setMessage(commonR.string.thread_export_not_connected)
.setPositiveButton(commonR.string.ok, null)
.show()
presenter.finishMatterThreadFlow()
}
MatterThreadStep.ERROR_THREAD_OTHER -> {
Toast.makeText(this@WebViewActivity, commonR.string.thread_export_unavailable, Toast.LENGTH_SHORT).show()
alertDialog?.cancel()
presenter.finishMatterThreadFlow()
}
else -> { } // Do nothing
}
Expand Down Expand Up @@ -702,6 +728,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
val pm: PackageManager = context.packageManager
val hasNfc = pm.hasSystemFeature(PackageManager.FEATURE_NFC)
val canCommissionMatter = presenter.appCanCommissionMatterDevice()
val canExportThread = presenter.appCanExportThreadCredentials()
webView.externalBus(
id = JSONObject(message).get("id"),
type = "result",
Expand All @@ -712,6 +739,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
"canWriteTag" to hasNfc,
"hasExoPlayer" to true,
"canCommissionMatter" to canCommissionMatter,
"canImportThreadCredentials" to canExportThread,
"hasAssist" to true
)
)
Expand Down Expand Up @@ -755,6 +783,14 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
)
)
"matter/commission" -> presenter.startCommissioningMatterDevice(this@WebViewActivity)
"thread/import_credentials" -> {
presenter.exportThreadCredentials(this@WebViewActivity)

alertDialog = AlertDialog.Builder(this@WebViewActivity)
.setMessage(commonR.string.thread_debug_active)
.create()
alertDialog?.show()
}
"exoplayer/play_hls" -> exoPlayHls(json)
"exoplayer/stop" -> exoStopHls()
"exoplayer/resize" -> exoResizeHls(json)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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

interface WebViewPresenter {
Expand Down Expand Up @@ -54,8 +53,12 @@ interface WebViewPresenter {

fun appCanCommissionMatterDevice(): Boolean
fun startCommissioningMatterDevice(context: Context)
fun getMatterCommissioningStatusFlow(): Flow<MatterFrontendCommissioningStatus>
fun getMatterCommissioningIntent(): IntentSender?
fun onMatterCommissioningIntentResult(context: Context, result: ActivityResult)
fun confirmMatterCommissioningError()

/** @return `true` if the app can send this device's preferred Thread credential to the server */
fun appCanExportThreadCredentials(): Boolean
fun exportThreadCredentials(context: Context)
fun getMatterThreadStepFlow(): Flow<MatterThreadStep>
fun getMatterThreadIntent(): IntentSender?
fun onMatterThreadIntentResult(context: Context, result: ActivityResult)
fun finishMatterThreadFlow()
}
Loading