Skip to content
Open
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 @@ -276,8 +276,8 @@ class SettingsWearViewModel @Inject constructor(private val serverManager: Serve
}
}

private fun readUriData(uri: String): ByteArray {
if (uri.isEmpty()) return ByteArray(0)
private fun readUriData(uri: String?): ByteArray {
if (uri.isNullOrEmpty()) return ByteArray(0)
return getApplication<HomeAssistantApplication>().contentResolver.openInputStream(
uri.toUri(),
)!!.buffered().use {
Expand All @@ -291,8 +291,8 @@ class SettingsWearViewModel @Inject constructor(private val serverManager: Serve
deviceName: String,
deviceTrackingEnabled: Boolean,
notificationsEnabled: Boolean,
tlsClientCertificateUri: String,
tlsClientCertificatePassword: String,
tlsClientCertificateUri: String?,
tlsClientCertificatePassword: String?,
) {
_hasData.value = false // Show loading indicator
val putDataRequest = PutDataMapRequest.create("/authenticate").run {
Expand All @@ -304,7 +304,7 @@ class SettingsWearViewModel @Inject constructor(private val serverManager: Serve
dataMap.putBoolean("LocationTracking", deviceTrackingEnabled)
dataMap.putBoolean("Notifications", notificationsEnabled)
dataMap.putByteArray("TLSClientCertificateData", readUriData(tlsClientCertificateUri))
dataMap.putString("TLSClientCertificatePassword", tlsClientCertificatePassword)
dataMap.putString("TLSClientCertificatePassword", tlsClientCertificatePassword.orEmpty())
setUrgent()
asPutDataRequest()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.wearable.Node
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.onboarding.OnboardApp
import io.homeassistant.companion.android.onboarding.WearOnboardApp
import io.homeassistant.companion.android.settings.wear.SettingsWearViewModel
import io.homeassistant.companion.android.util.enableEdgeToEdgeCompat
import kotlinx.coroutines.cancel
Expand All @@ -23,7 +22,7 @@ class SettingsWearMainView : AppCompatActivity() {
private val settingsWearViewModel by viewModels<SettingsWearViewModel>()

private val registerActivityResult = registerForActivityResult(
OnboardApp(),
WearOnboardApp(),
this::onOnboardingComplete,
)

Expand Down Expand Up @@ -69,28 +68,21 @@ class SettingsWearMainView : AppCompatActivity() {

private fun loginWearOs() {
registerActivityResult.launch(
OnboardApp.Input(
WearOnboardApp.Input(
url = registerUrl,
defaultDeviceName = currentNodes.firstOrNull()?.displayName ?: "unknown",
locationTrackingPossible = false,
// While notifications are technically possible, the app can't handle this for the Wear device
notificationsPossible = false,
isWatch = true,
discoveryOptions = OnboardApp.DiscoveryOptions.ADD_EXISTING_EXTERNAL,
mayRequireTlsClientCertificate =
(application as HomeAssistantApplication).keyChainRepository.getPrivateKey() != null,
),
)
}

private fun onOnboardingComplete(result: OnboardApp.Output?) {
private fun onOnboardingComplete(result: WearOnboardApp.Output?) {
result?.apply {
settingsWearViewModel.sendAuthToWear(
url,
authCode,
deviceName,
deviceTrackingEnabled,
true,
deviceTrackingEnabled = false,
notificationsEnabled = true,
tlsClientCertificateUri,
tlsClientCertificatePassword,
)
Expand Down
1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- TODO validate that new onboarding works properly on TV -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.homeassistant.companion.android.onboarding

import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.activity.result.contract.ActivityResultContract
import io.homeassistant.companion.android.launcher.intentLauncherWearOnboarding

class WearOnboardApp : ActivityResultContract<WearOnboardApp.Input, WearOnboardApp.Output?>() {
data class Input(val url: String? = null, val defaultDeviceName: String = Build.MODEL)

data class Output(
val url: String,
val authCode: String,
val deviceName: String,
val tlsClientCertificateUri: String?,
val tlsClientCertificatePassword: String?,
) {
fun toIntent(): Intent {
return Intent().apply {
putExtra(EXTRA_OUTPUT_URL, url)
putExtra(EXTRA_OUTPUT_AUTH_CODE, authCode)
putExtra(EXTRA_OUTPUT_DEVICE_NAME, deviceName)
putExtra(EXTRA_OUTPUT_TLS_CLIENT_CERTIFICATE_URI, tlsClientCertificateUri)
putExtra(EXTRA_OUTPUT_TLS_CLIENT_CERTIFICATE_PASSWORD, tlsClientCertificatePassword)
}
}

companion object {
private const val EXTRA_OUTPUT_URL = "URL"
private const val EXTRA_OUTPUT_AUTH_CODE = "AuthCode"
private const val EXTRA_OUTPUT_DEVICE_NAME = "DeviceName"
private const val EXTRA_OUTPUT_TLS_CLIENT_CERTIFICATE_URI = "TLSClientCertificateUri"
private const val EXTRA_OUTPUT_TLS_CLIENT_CERTIFICATE_PASSWORD = "TLSClientCertificatePassword"

fun fromIntent(intent: Intent): Output {
return Output(
url = intent.getStringExtra(EXTRA_OUTPUT_URL).toString(),
authCode = intent.getStringExtra(EXTRA_OUTPUT_AUTH_CODE).toString(),
deviceName = intent.getStringExtra(EXTRA_OUTPUT_DEVICE_NAME).toString(),
tlsClientCertificateUri = intent.getStringExtra(EXTRA_OUTPUT_TLS_CLIENT_CERTIFICATE_URI),
tlsClientCertificatePassword = intent.getStringExtra(EXTRA_OUTPUT_TLS_CLIENT_CERTIFICATE_PASSWORD),
)
}
}
}

override fun createIntent(context: Context, input: Input): Intent {
return context.intentLauncherWearOnboarding(input.defaultDeviceName, input.url)
}

override fun parseResult(resultCode: Int, intent: Intent?): Output? {
if (intent == null) {
return null
}

return Output.fromIntent(intent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ internal sealed interface NameYourDeviceNavigationEvent {
data class Error(@StringRes val messageRes: Int) : NameYourDeviceNavigationEvent
}

/**
* ViewModel for the Name Your Device screen during phone/tablet onboarding.
*
* **Note:** This ViewModel is NOT used during Wear OS onboarding. The Wear onboarding flow
* uses the screen without this view model since it handles device naming differently, as it returns the result
* directly to the phone app via [io.homeassistant.companion.android.onboarding.WearOnboardApp.Output].
*/
@HiltViewModel
internal class NameYourDeviceViewModel @VisibleForTesting constructor(
private val route: NameYourDeviceRoute,
Expand Down Expand Up @@ -172,8 +179,13 @@ internal class NameYourDeviceViewModel @VisibleForTesting constructor(
messagingTokenProvider(),
),
)
return serverManager.convertTemporaryServer(tempServerId)
val serverId = serverManager.convertTemporaryServer(tempServerId)
?: throw IllegalStateException("Server still temporary")

// Active the newly added server
serverManager.activateServer(serverId)

return serverId
} catch (e: Exception) {
// Fatal errors: if one of these calls fail, the app cannot proceed.
// Show an error, clean up the session and require new registration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ internal fun NavGraphBuilder.nameYourWearDeviceScreen(
// If the logic of the screen evolve we should consider introducing a viewModel instead.
var deviceName by rememberSaveable { mutableStateOf(route.defaultDeviceName) }

// Unlike phone/tablet onboarding, we don't use NameYourDeviceViewModel here.
// In Wear onboarding, the phone/tablet acts as a proxy: it collects the device name and auth code,
// then forwards them to the watch via WearOnboardApp.Output. The watch itself handles
// server registration, not this device.
NameYourDeviceScreen(
onBackClick = onBackClick,
onHelpClick = onHelpClick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ import io.homeassistant.companion.android.common.util.isAutomotive
import io.homeassistant.companion.android.common.util.isIgnoringBatteryOptimizations
import io.homeassistant.companion.android.common.util.maybeAskForIgnoringBatteryOptimizations
import io.homeassistant.companion.android.database.server.Server
import io.homeassistant.companion.android.launcher.intentLauncherOnboarding
import io.homeassistant.companion.android.nfc.NfcSetupActivity
import io.homeassistant.companion.android.onboarding.OnboardApp
import io.homeassistant.companion.android.settings.controls.ManageControlsSettingsFragment
import io.homeassistant.companion.android.settings.developer.DeveloperSettingsFragment
import io.homeassistant.companion.android.settings.gestures.GesturesFragment
Expand Down Expand Up @@ -77,9 +77,6 @@ class SettingsFragment(private val presenter: SettingsPresenter, private val lan
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
updateNotificationChannelPrefs()
}

private val requestOnboardingResult = registerForActivityResult(OnboardApp(), this::onOnboardingComplete)

private var serverAuth: Int? = null
private val serverMutex = Mutex()

Expand Down Expand Up @@ -140,13 +137,15 @@ class SettingsFragment(private val presenter: SettingsPresenter, private val lan

findPreference<Preference>("server_add")?.let {
it.setOnPreferenceClickListener {
requestOnboardingResult.launch(
OnboardApp.Input(
// Empty url skips the 'Welcome' screen
url = "",
discoveryOptions = OnboardApp.DiscoveryOptions.HIDE_EXISTING,
),
)
requireContext().apply {
startActivity(
intentLauncherOnboarding(
urlToOnboard = null,
hideExistingServers = true,
skipWelcome = true,
),
)
}
return@setOnPreferenceClickListener true
}
}
Expand Down Expand Up @@ -560,12 +559,6 @@ class SettingsFragment(private val presenter: SettingsPresenter, private val lan
return true
}

private fun onOnboardingComplete(result: OnboardApp.Output?) {
lifecycleScope.launch {
presenter.addServer(result)
}
}

private fun openNotificationSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requestNotificationPermissionResult.launch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.content.Context
import androidx.preference.PreferenceDataStore
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse
import io.homeassistant.companion.android.database.server.Server
import io.homeassistant.companion.android.onboarding.OnboardApp
import kotlinx.coroutines.flow.StateFlow

interface SettingsPresenter {
Expand All @@ -18,7 +17,6 @@ interface SettingsPresenter {
fun onFinish()
fun updateSuggestions(context: Context)
fun cancelSuggestion(context: Context, id: String)
suspend fun addServer(result: OnboardApp.Output?)
fun getSuggestionFlow(): StateFlow<SettingsHomeSuggestion?>
fun getServersFlow(): StateFlow<List<Server>>
fun getServerCount(): Int
Expand Down
Loading