Smarter discovery for already added instances (#3723)

- Hide already added instances when adding another server to the app when found in discovery
 - Show already added instances with their external URL in discovery when logging in a Wear OS device
This commit is contained in:
Joris Pelgröm 2023-07-28 19:52:29 +02:00 committed by GitHub
parent 447ad5e30a
commit 7b40dec713
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 95 additions and 13 deletions

View file

@ -70,7 +70,8 @@ class SettingsWearMainView : AppCompatActivity() {
defaultDeviceName = currentNodes.firstOrNull()?.displayName ?: "unknown",
locationTrackingPossible = false,
notificationsPossible = false,
isWatch = true
isWatch = true,
discoveryOptions = OnboardApp.DiscoveryOptions.ADD_EXISTING_EXTERNAL
) // While notifications are technically possible, the app can't handle this for the Wear device
)
}

View file

@ -14,22 +14,33 @@ class OnboardApp : ActivityResultContract<OnboardApp.Input, OnboardApp.Output?>(
private const val EXTRA_LOCATION_TRACKING_POSSIBLE = "location_tracking_possible"
private const val EXTRA_NOTIFICATIONS_POSSIBLE = "notifications_possible"
private const val EXTRA_IS_WATCH = "extra_is_watch"
private const val EXTRA_DISCOVERY_OPTIONS = "extra_discovery_options"
fun parseInput(intent: Intent): Input = Input(
url = intent.getStringExtra(EXTRA_URL),
defaultDeviceName = intent.getStringExtra(EXTRA_DEFAULT_DEVICE_NAME) ?: Build.MODEL,
locationTrackingPossible = intent.getBooleanExtra(EXTRA_LOCATION_TRACKING_POSSIBLE, false),
notificationsPossible = intent.getBooleanExtra(EXTRA_NOTIFICATIONS_POSSIBLE, true),
isWatch = intent.getBooleanExtra(EXTRA_IS_WATCH, false)
isWatch = intent.getBooleanExtra(EXTRA_IS_WATCH, false),
discoveryOptions = intent.getStringExtra(EXTRA_DISCOVERY_OPTIONS)?.let { DiscoveryOptions.valueOf(it) }
)
}
enum class DiscoveryOptions {
/** Add existing servers in the app to discovery results using their external URL */
ADD_EXISTING_EXTERNAL,
/** Hide existing servers in the app from discovery results if discovered */
HIDE_EXISTING
}
data class Input(
val url: String? = null,
val defaultDeviceName: String = Build.MODEL,
val locationTrackingPossible: Boolean = BuildConfig.FLAVOR == "full",
val notificationsPossible: Boolean = true,
val isWatch: Boolean = false
val isWatch: Boolean = false,
val discoveryOptions: DiscoveryOptions? = null
)
data class Output(
@ -57,6 +68,7 @@ class OnboardApp : ActivityResultContract<OnboardApp.Input, OnboardApp.Output?>(
putExtra(EXTRA_LOCATION_TRACKING_POSSIBLE, input.locationTrackingPossible)
putExtra(EXTRA_NOTIFICATIONS_POSSIBLE, input.notificationsPossible)
putExtra(EXTRA_IS_WATCH, input.isWatch)
putExtra(EXTRA_DISCOVERY_OPTIONS, input.discoveryOptions?.toString())
}
}

View file

@ -43,6 +43,7 @@ class OnboardingActivity : BaseActivity() {
false
}
viewModel.deviceIsWatch = input.isWatch
viewModel.discoveryOptions = input.discoveryOptions
if (savedInstanceState == null) {
supportFragmentManager.commit {

View file

@ -1,6 +1,7 @@
package io.homeassistant.companion.android.onboarding
import android.app.Application
import android.util.Log
import android.webkit.URLUtil
import android.widget.Toast
import androidx.compose.runtime.getValue
@ -12,23 +13,27 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LifecycleObserver
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.server.ServerConnectionInfo
import io.homeassistant.companion.android.onboarding.discovery.HomeAssistantInstance
import io.homeassistant.companion.android.onboarding.discovery.HomeAssistantSearcher
import java.net.URL
import javax.inject.Inject
@HiltViewModel
class OnboardingViewModel @Inject constructor(
val serverManager: ServerManager,
app: Application
) : AndroidViewModel(app) {
companion object {
const val TAG = "OnboardingViewModel"
}
private val _homeAssistantSearcher = HomeAssistantSearcher(
nsdManager = app.getSystemService()!!,
wifiManager = app.getSystemService(),
onInstanceFound = { instance ->
if (foundInstances.none { it.url == instance.url }) {
foundInstances.add(instance)
}
},
onInstanceFound = ::onInstanceFound,
onError = {
Toast.makeText(app, R.string.failed_scan, Toast.LENGTH_LONG).show()
// TODO: Go to manual setup?
@ -38,6 +43,7 @@ class OnboardingViewModel @Inject constructor(
val foundInstances = mutableStateListOf<HomeAssistantInstance>()
val manualUrl = mutableStateOf("")
var discoveryOptions: OnboardApp.DiscoveryOptions? = null
var manualContinueEnabled by mutableStateOf(false)
private set
var authCode by mutableStateOf("")
@ -78,7 +84,50 @@ class OnboardingViewModel @Inject constructor(
notificationsEnabled = notificationsEnabled
)
fun onDiscoveryActive() {
if (discoveryOptions != OnboardApp.DiscoveryOptions.ADD_EXISTING_EXTERNAL || !foundInstances.isEmpty()) return
serverManager.defaultServers.forEach {
val url = it.connection.getUrl(isInternal = false) ?: return@forEach
val version = it.version ?: return@forEach
foundInstances.add(
HomeAssistantInstance(
name = it.friendlyName,
url = url,
version = version
)
)
}
}
private fun onInstanceFound(instance: HomeAssistantInstance) {
if (
(discoveryOptions == OnboardApp.DiscoveryOptions.ADD_EXISTING_EXTERNAL || discoveryOptions == OnboardApp.DiscoveryOptions.HIDE_EXISTING) &&
serverManager.defaultServers.any { it.connection.hasUrl(instance.url) }
) {
// Skip anything with a URL known to the app, as it is added initially or should be hidden
Log.i(TAG, "Skipping instance ${instance.name} (${instance.url}) because of option $discoveryOptions")
return
}
if (foundInstances.none { it.url == instance.url }) foundInstances.add(instance)
}
override fun onCleared() {
_homeAssistantSearcher.stopSearch()
}
private fun ServerConnectionInfo.hasUrl(url: URL): Boolean {
val urls = listOf(internalUrl, externalUrl, cloudUrl)
urls.forEach {
if (it.isNullOrBlank()) return@forEach
try {
val parsed = URL(it)
if (parsed.protocol == url.protocol && parsed.host == url.host && parsed.port == url.port) return true
} catch (e: Exception) {
// Unable to compare
}
}
return false
}
}

View file

@ -7,12 +7,16 @@ import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.onboarding.OnboardingViewModel
import io.homeassistant.companion.android.onboarding.authentication.AuthenticationFragment
import io.homeassistant.companion.android.onboarding.manual.ManualSetupFragment
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@ -20,6 +24,15 @@ class DiscoveryFragment @Inject constructor() : Fragment() {
private val viewModel by activityViewModels<OnboardingViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onDiscoveryActive()
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,

View file

@ -1,9 +1,10 @@
package io.homeassistant.companion.android.onboarding.discovery
import io.homeassistant.companion.android.common.data.HomeAssistantVersion
import java.net.URL
data class HomeAssistantInstance(
val name: String,
val url: URL,
val version: String
val version: HomeAssistantVersion
)

View file

@ -6,6 +6,7 @@ import android.net.wifi.WifiManager
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import io.homeassistant.companion.android.common.data.HomeAssistantVersion
import okio.internal.commonToUtf8String
import java.net.URL
import java.util.concurrent.locks.ReentrantLock
@ -29,7 +30,7 @@ class HomeAssistantSearcher constructor(
private var multicastLock: WifiManager.MulticastLock? = null
fun beginSearch() {
private fun beginSearch() {
if (isSearching) {
return
}
@ -93,13 +94,14 @@ class HomeAssistantSearcher constructor(
Log.i(TAG, "Service resolved: $resolvedService")
resolvedService?.let {
val baseUrl = it.attributes["base_url"]
val version = it.attributes["version"]
val versionAttr = it.attributes["version"]
val version = versionAttr?.let { ver -> HomeAssistantVersion.fromString(ver.commonToUtf8String()) }
if (baseUrl != null && version != null) {
onInstanceFound(
HomeAssistantInstance(
it.serviceName,
URL(baseUrl.commonToUtf8String()),
version.commonToUtf8String()
version
)
)
}

View file

@ -136,7 +136,10 @@ class SettingsFragment(
findPreference<Preference>("server_add")?.let {
it.setOnPreferenceClickListener {
requestOnboardingResult.launch(
OnboardApp.Input(url = "") // Empty url skips the 'Welcome' screen
OnboardApp.Input(
url = "", // Empty url skips the 'Welcome' screen
discoveryOptions = OnboardApp.DiscoveryOptions.HIDE_EXISTING
)
)
return@setOnPreferenceClickListener true
}