mirror of
https://github.com/home-assistant/android
synced 2024-10-04 15:19:30 +00:00
Support for Thread "import credentials" from frontend (#4128)
* Support for Thread "import credentials" frontend action - Add support for the frontend action to import Thread credentials, or better said export Thread credentials from the device. This is essentially a very basic sync. * Rename/reorder reused Matter code - Change names and order of reused Matter code for Thread importing to prevent potential confusion * Update minimal implementation
This commit is contained in:
parent
8dd76734f8
commit
a5efe6a9ab
|
@ -97,6 +97,7 @@ class MatterCommissioningViewModel @Inject constructor(
|
|||
val result = threadManager.syncPreferredDataset(
|
||||
getApplication<Application>().applicationContext,
|
||||
serverId,
|
||||
false,
|
||||
viewModelScope
|
||||
)
|
||||
when (result) {
|
||||
|
|
|
@ -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
|
||||
|
@ -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) }
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package io.homeassistant.companion.android.matter
|
||||
|
||||
enum class MatterFrontendCommissioningStatus {
|
||||
NOT_STARTED,
|
||||
REQUESTED,
|
||||
THREAD_EXPORT_TO_SERVER,
|
||||
IN_PROGRESS,
|
||||
ERROR
|
||||
}
|
|
@ -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 -> {
|
||||
|
|
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import io.homeassistant.companion.android.common.data.authentication.SessionStat
|
|||
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
|
||||
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.UrlUtil
|
||||
|
@ -58,9 +57,9 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
private var url: URL? = null
|
||||
private var urlForServer: Int? = null
|
||||
|
||||
private val _matterCommissioningStatus = MutableStateFlow(MatterFrontendCommissioningStatus.NOT_STARTED)
|
||||
private val _matterThreadStep = MutableStateFlow(MatterThreadStep.NOT_STARTED)
|
||||
|
||||
private var matterCommissioningIntentSender: IntentSender? = null
|
||||
private var matterThreadIntentSender: IntentSender? = null
|
||||
|
||||
init {
|
||||
updateActiveServer()
|
||||
|
@ -343,12 +342,12 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
override fun appCanCommissionMatterDevice(): Boolean = matterUseCase.appSupportsCommissioning()
|
||||
|
||||
override fun startCommissioningMatterDevice(context: Context) {
|
||||
if (_matterCommissioningStatus.value != MatterFrontendCommissioningStatus.REQUESTED) {
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.REQUESTED)
|
||||
if (_matterThreadStep.value != MatterThreadStep.REQUESTED) {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.REQUESTED)
|
||||
|
||||
mainScope.launch {
|
||||
val deviceThreadIntent = try {
|
||||
when (val result = threadUseCase.syncPreferredDataset(context, serverId, CoroutineScope(coroutineContext + SupervisorJob()))) {
|
||||
when (val result = threadUseCase.syncPreferredDataset(context, serverId, false, CoroutineScope(coroutineContext + SupervisorJob()))) {
|
||||
is ThreadManager.SyncResult.OnlyOnDevice -> result.exportIntent
|
||||
is ThreadManager.SyncResult.AllHaveCredentials -> result.exportIntent
|
||||
else -> null
|
||||
|
@ -358,8 +357,8 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
null
|
||||
}
|
||||
if (deviceThreadIntent != null) {
|
||||
matterCommissioningIntentSender = deviceThreadIntent
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER)
|
||||
matterThreadIntentSender = deviceThreadIntent
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.THREAD_EXPORT_TO_SERVER_MATTER)
|
||||
} else {
|
||||
startMatterCommissioningFlow(context)
|
||||
}
|
||||
|
@ -372,33 +371,79 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
context,
|
||||
{ intentSender ->
|
||||
Log.d(TAG, "Matter commissioning is ready")
|
||||
matterCommissioningIntentSender = intentSender
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.IN_PROGRESS)
|
||||
matterThreadIntentSender = intentSender
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.MATTER_IN_PROGRESS)
|
||||
},
|
||||
{ e ->
|
||||
Log.e(TAG, "Matter commissioning couldn't be prepared", e)
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.ERROR)
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.ERROR_MATTER)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMatterCommissioningStatusFlow(): Flow<MatterFrontendCommissioningStatus> =
|
||||
_matterCommissioningStatus.asStateFlow()
|
||||
override fun appCanExportThreadCredentials(): Boolean = threadUseCase.appSupportsThread()
|
||||
|
||||
override fun getMatterCommissioningIntent(): IntentSender? {
|
||||
val intent = matterCommissioningIntentSender
|
||||
matterCommissioningIntentSender = null
|
||||
override fun exportThreadCredentials(context: Context) {
|
||||
if (_matterThreadStep.value != MatterThreadStep.REQUESTED) {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.REQUESTED)
|
||||
|
||||
mainScope.launch {
|
||||
try {
|
||||
val result = threadUseCase.syncPreferredDataset(context, serverId, true, CoroutineScope(coroutineContext + SupervisorJob()))
|
||||
Log.d(TAG, "Export preferred Thread dataset returned $result")
|
||||
|
||||
when (result) {
|
||||
is ThreadManager.SyncResult.OnlyOnDevice -> {
|
||||
matterThreadIntentSender = result.exportIntent
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.THREAD_EXPORT_TO_SERVER_ONLY)
|
||||
}
|
||||
is ThreadManager.SyncResult.NoneHaveCredentials,
|
||||
is ThreadManager.SyncResult.OnlyOnServer -> {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.THREAD_NONE)
|
||||
}
|
||||
is ThreadManager.SyncResult.NotConnected -> {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.ERROR_THREAD_LOCAL_NETWORK)
|
||||
}
|
||||
else -> {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.ERROR_THREAD_OTHER)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to export preferred Thread dataset", e)
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.ERROR_THREAD_OTHER)
|
||||
}
|
||||
}
|
||||
} // else already waiting for a result, don't send another request
|
||||
}
|
||||
|
||||
override fun getMatterThreadStepFlow(): Flow<MatterThreadStep> =
|
||||
_matterThreadStep.asStateFlow()
|
||||
|
||||
override fun getMatterThreadIntent(): IntentSender? {
|
||||
val intent = matterThreadIntentSender
|
||||
matterThreadIntentSender = null
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun onMatterCommissioningIntentResult(context: Context, result: ActivityResult) {
|
||||
when (_matterCommissioningStatus.value) {
|
||||
MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER -> {
|
||||
override fun onMatterThreadIntentResult(context: Context, result: ActivityResult) {
|
||||
when (_matterThreadStep.value) {
|
||||
MatterThreadStep.THREAD_EXPORT_TO_SERVER_MATTER -> {
|
||||
mainScope.launch {
|
||||
threadUseCase.sendThreadDatasetExportResult(result, serverId)
|
||||
startMatterCommissioningFlow(context)
|
||||
}
|
||||
}
|
||||
MatterThreadStep.THREAD_EXPORT_TO_SERVER_ONLY -> {
|
||||
mainScope.launch {
|
||||
val sent = threadUseCase.sendThreadDatasetExportResult(result, serverId)
|
||||
Log.d(TAG, "Thread ${if (!sent.isNullOrBlank()) "sent credential for $sent" else "did not send credential"}")
|
||||
if (sent.isNullOrBlank()) {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.THREAD_NONE)
|
||||
} else {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.THREAD_SENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Any errors will have been shown in the UI provided by Play Services
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
|
@ -410,7 +455,7 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun confirmMatterCommissioningError() {
|
||||
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.NOT_STARTED)
|
||||
override fun finishMatterThreadFlow() {
|
||||
_matterThreadStep.tryEmit(MatterThreadStep.NOT_STARTED)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ class ThreadManagerImpl @Inject constructor() : ThreadManager {
|
|||
override suspend fun syncPreferredDataset(
|
||||
context: Context,
|
||||
serverId: Int,
|
||||
exportOnly: Boolean,
|
||||
scope: CoroutineScope
|
||||
): ThreadManager.SyncResult = ThreadManager.SyncResult.AppUnsupported
|
||||
|
||||
|
|
|
@ -1145,6 +1145,10 @@
|
|||
<string name="thread_debug_result_unsupported_server">The Home Assistant server does not support Thread</string>
|
||||
<string name="thread_debug_result_updated">Updated network from Home Assistant on this device</string>
|
||||
<string name="thread_debug_summary">Manually update device and server Thread credentials and verify results</string>
|
||||
<string name="thread_export_success">Imported credential</string>
|
||||
<string name="thread_export_none">You don\'t have any credentials to import.</string>
|
||||
<string name="thread_export_not_connected">You are not connected to a local network. Connect to Wi-Fi or ethernet to import Thread credentials.</string>
|
||||
<string name="thread_export_unavailable">Thread is currently unavailable</string>
|
||||
<string name="tile_vibrate">Vibrate when clicked</string>
|
||||
<string name="tile_auth_required">Requires unlocked device</string>
|
||||
<string name="no_results">No results yet</string>
|
||||
|
|
Loading…
Reference in a new issue