refactor: offer ABox and local experience providers
This commit is contained in:
+10
-12
@@ -2,7 +2,6 @@ package com.flagship.abox.manager.ui.provider
|
|||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.assertIsEnabled
|
import androidx.compose.ui.test.assertIsEnabled
|
||||||
import androidx.compose.ui.test.assertIsNotEnabled
|
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
@@ -22,27 +21,26 @@ class ProviderSelectionScreenTest {
|
|||||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun enabledOptionCanBeSelectedAndDisabledOptionCannot() {
|
fun aboxAndLocalExperienceAreBothSelectableAndNoCompatibleOptionIsShown() {
|
||||||
val local = ProviderOption(
|
val abox = ProviderProfile(ProviderProfileId("abox"), ProviderType.ABOX, "ABox")
|
||||||
ProviderProfile(ProviderProfileId("local"), ProviderType.LOCAL_EXPERIENCE, "Local"),
|
val local = ProviderProfile(
|
||||||
Availability.Enabled,
|
ProviderProfileId("local"),
|
||||||
|
ProviderType.LOCAL_EXPERIENCE,
|
||||||
|
"Local",
|
||||||
)
|
)
|
||||||
val abox = ProviderOption(
|
var selected: ProviderProfile? = null
|
||||||
ProviderProfile(ProviderProfileId("abox"), ProviderType.ABOX, "ABox"),
|
|
||||||
Availability.Disabled(UnavailableReason.PROTOCOL_PENDING),
|
|
||||||
)
|
|
||||||
var selected: ProviderOption? = null
|
|
||||||
composeRule.setContent {
|
composeRule.setContent {
|
||||||
ABoxTheme {
|
ABoxTheme {
|
||||||
ProviderSelectionScreen(
|
ProviderSelectionScreen(
|
||||||
state = ProviderSelectionUiState(listOf(local, abox)),
|
state = ProviderSelectionUiState(listOf(abox, local)),
|
||||||
onSelect = { selected = it },
|
onSelect = { selected = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composeRule.onNodeWithText("ABox").assertIsEnabled()
|
||||||
composeRule.onNodeWithText("Local experience").assertIsEnabled().performClick()
|
composeRule.onNodeWithText("Local experience").assertIsEnabled().performClick()
|
||||||
assertEquals(local, selected)
|
assertEquals(local, selected)
|
||||||
composeRule.onNodeWithText("ABox").assertIsNotEnabled()
|
composeRule.onNodeWithText("Compatible backend").assertDoesNotExist()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ import com.flagship.abox.manager.session.credential.KeystoreCredentialStore
|
|||||||
import com.flagship.abox.manager.session.profile.ProviderProfileStoreImpl
|
import com.flagship.abox.manager.session.profile.ProviderProfileStoreImpl
|
||||||
import com.flagship.abox.manager.session.profile.sessionDataStore
|
import com.flagship.abox.manager.session.profile.sessionDataStore
|
||||||
import com.flagship.abox.manager.simulation.SimulationSessionGateway
|
import com.flagship.abox.manager.simulation.SimulationSessionGateway
|
||||||
import com.flagship.abox.manager.ui.provider.Availability
|
|
||||||
import com.flagship.abox.manager.ui.provider.ProviderAvailability
|
|
||||||
import com.flagship.abox.manager.ui.provider.UnavailableReason
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -67,13 +64,6 @@ object SessionModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideLocalSessionGateway(): LocalSessionGateway = SimulationSessionGateway()
|
fun provideLocalSessionGateway(): LocalSessionGateway = SimulationSessionGateway()
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideProviderAvailability(): ProviderAvailability = ProviderAvailability(
|
|
||||||
abox = Availability.Enabled,
|
|
||||||
compatible = Availability.Disabled(UnavailableReason.PROTOCOL_PENDING),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideSessionCoordinator(
|
fun provideSessionCoordinator(
|
||||||
|
|||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
package com.flagship.abox.manager.ui.provider
|
|
||||||
|
|
||||||
import com.flagship.abox.manager.domain.ProviderType
|
|
||||||
|
|
||||||
data class ProviderAvailability(
|
|
||||||
val abox: Availability,
|
|
||||||
val compatible: Availability,
|
|
||||||
val local: Availability = Availability.Enabled,
|
|
||||||
) {
|
|
||||||
fun forType(type: ProviderType): Availability = when (type) {
|
|
||||||
ProviderType.ABOX -> abox
|
|
||||||
ProviderType.LOCAL_EXPERIENCE -> local
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface Availability {
|
|
||||||
data object Enabled : Availability
|
|
||||||
|
|
||||||
data object Simulation : Availability
|
|
||||||
|
|
||||||
data class Disabled(val reason: UnavailableReason) : Availability
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class UnavailableReason {
|
|
||||||
PROTOCOL_PENDING,
|
|
||||||
TRANSPORT_SECURITY_PENDING,
|
|
||||||
}
|
|
||||||
+20
-59
@@ -24,7 +24,6 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
@@ -36,6 +35,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.flagship.abox.manager.R
|
import com.flagship.abox.manager.R
|
||||||
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
|
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfile
|
||||||
import com.flagship.abox.manager.domain.ProviderType
|
import com.flagship.abox.manager.domain.ProviderType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -50,7 +50,7 @@ fun ProviderSelectionRoute(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ProviderSelectionScreen(
|
fun ProviderSelectionScreen(
|
||||||
state: ProviderSelectionUiState,
|
state: ProviderSelectionUiState,
|
||||||
onSelect: (ProviderOption) -> Unit,
|
onSelect: (ProviderProfile) -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -77,9 +77,9 @@ fun ProviderSelectionScreen(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(bottom = 6.dp),
|
modifier = Modifier.padding(bottom = 6.dp),
|
||||||
)
|
)
|
||||||
state.providers.forEach { option ->
|
state.providers.forEach { profile ->
|
||||||
ProviderOptionCard(
|
ProviderOptionCard(
|
||||||
option = option,
|
profile = profile,
|
||||||
selecting = state.isSelecting,
|
selecting = state.isSelecting,
|
||||||
onSelect = onSelect,
|
onSelect = onSelect,
|
||||||
)
|
)
|
||||||
@@ -102,33 +102,25 @@ fun ProviderSelectionScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProviderOptionCard(
|
private fun ProviderOptionCard(
|
||||||
option: ProviderOption,
|
profile: ProviderProfile,
|
||||||
selecting: Boolean,
|
selecting: Boolean,
|
||||||
onSelect: (ProviderOption) -> Unit,
|
onSelect: (ProviderProfile) -> Unit,
|
||||||
) {
|
) {
|
||||||
val enabled = option.availability !is Availability.Disabled && !selecting
|
val displayName = providerName(profile.type)
|
||||||
val displayName = providerName(option.profile.type)
|
val description = providerDescription(profile.type)
|
||||||
val description = providerDescription(option.profile.type)
|
|
||||||
val status = availabilityLabel(option.availability)
|
|
||||||
val statusColor = availabilityColor(option.availability)
|
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
onClick = { onSelect(option) },
|
onClick = { onSelect(profile) },
|
||||||
enabled = enabled,
|
enabled = !selecting,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
colors = CardDefaults.outlinedCardColors(
|
colors = CardDefaults.outlinedCardColors(
|
||||||
containerColor = when (option.availability) {
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
Availability.Enabled -> MaterialTheme.colorScheme.surface
|
|
||||||
Availability.Simulation -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f)
|
|
||||||
is Availability.Disabled -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f)
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.alpha(if (enabled || selecting) 1f else 0.72f)
|
|
||||||
.semantics {
|
.semantics {
|
||||||
role = Role.Button
|
role = Role.Button
|
||||||
contentDescription = "$displayName, $status"
|
contentDescription = displayName
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -140,13 +132,13 @@ private fun ProviderOptionCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(statusColor.copy(alpha = 0.16f)),
|
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.16f)),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = providerMark(option.profile.type),
|
text = providerMark(profile.type),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = statusColor,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
@@ -159,24 +151,12 @@ private fun ProviderOptionCard(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.size(8.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(statusColor),
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
status,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = statusColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Text("›", style = MaterialTheme.typography.headlineSmall, color = statusColor)
|
Text(
|
||||||
|
"›",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,22 +181,3 @@ private fun providerMark(type: ProviderType): String = when (type) {
|
|||||||
ProviderType.ABOX -> "A"
|
ProviderType.ABOX -> "A"
|
||||||
ProviderType.LOCAL_EXPERIENCE -> "L"
|
ProviderType.LOCAL_EXPERIENCE -> "L"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun availabilityLabel(availability: Availability): String = stringResource(
|
|
||||||
when (availability) {
|
|
||||||
Availability.Enabled -> R.string.provider_status_available
|
|
||||||
Availability.Simulation -> R.string.provider_status_simulation
|
|
||||||
is Availability.Disabled -> when (availability.reason) {
|
|
||||||
UnavailableReason.PROTOCOL_PENDING -> R.string.provider_status_protocol_pending
|
|
||||||
UnavailableReason.TRANSPORT_SECURITY_PENDING -> R.string.provider_status_transport_pending
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun availabilityColor(availability: Availability) = when (availability) {
|
|
||||||
Availability.Enabled -> MaterialTheme.colorScheme.primary
|
|
||||||
Availability.Simulation -> MaterialTheme.colorScheme.tertiary
|
|
||||||
is Availability.Disabled -> MaterialTheme.colorScheme.error
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-19
@@ -2,6 +2,7 @@ package com.flagship.abox.manager.ui.provider
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
import com.flagship.abox.manager.domain.ProviderProfile
|
import com.flagship.abox.manager.domain.ProviderProfile
|
||||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
import com.flagship.abox.manager.domain.ProviderType
|
import com.flagship.abox.manager.domain.ProviderType
|
||||||
@@ -16,26 +17,18 @@ import kotlinx.coroutines.launch
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ProviderSelectionViewModel @Inject constructor(
|
class ProviderSelectionViewModel @Inject constructor(
|
||||||
private val coordinator: SessionCoordinator,
|
private val coordinator: SessionCoordinator,
|
||||||
availability: ProviderAvailability,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _uiState = MutableStateFlow(
|
private val _uiState = MutableStateFlow(ProviderSelectionUiState(providers = PROFILES))
|
||||||
ProviderSelectionUiState(
|
|
||||||
providers = PROFILES.map { profile ->
|
|
||||||
ProviderOption(profile, availability.forType(profile.type))
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val uiState: StateFlow<ProviderSelectionUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ProviderSelectionUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun select(option: ProviderOption) {
|
fun select(profile: ProviderProfile) {
|
||||||
if (option.availability is Availability.Disabled || _uiState.value.isSelecting) return
|
if (_uiState.value.isSelecting) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isSelecting = true, errorMessage = null)
|
_uiState.value = _uiState.value.copy(isSelecting = true, errorMessage = null)
|
||||||
val result = coordinator.selectProvider(option.profile)
|
val result = coordinator.selectProvider(profile)
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
isSelecting = false,
|
isSelecting = false,
|
||||||
errorMessage = (result as? com.flagship.abox.manager.domain.DomainResult.Failure)
|
errorMessage = (result as? DomainResult.Failure)?.error?.detail,
|
||||||
?.error?.detail,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,12 +46,7 @@ class ProviderSelectionViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ProviderSelectionUiState(
|
data class ProviderSelectionUiState(
|
||||||
val providers: List<ProviderOption>,
|
val providers: List<ProviderProfile>,
|
||||||
val isSelecting: Boolean = false,
|
val isSelecting: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ProviderOption(
|
|
||||||
val profile: ProviderProfile,
|
|
||||||
val availability: Availability,
|
|
||||||
)
|
|
||||||
|
|||||||
+4
-16
@@ -8,8 +8,6 @@ import com.flagship.abox.manager.domain.ProviderProfileId
|
|||||||
import com.flagship.abox.manager.domain.ProviderProfileStore
|
import com.flagship.abox.manager.domain.ProviderProfileStore
|
||||||
import com.flagship.abox.manager.domain.Session
|
import com.flagship.abox.manager.domain.Session
|
||||||
import com.flagship.abox.manager.domain.SessionCoordinator
|
import com.flagship.abox.manager.domain.SessionCoordinator
|
||||||
import com.flagship.abox.manager.ui.provider.Availability
|
|
||||||
import com.flagship.abox.manager.ui.provider.ProviderAvailability
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -25,7 +23,6 @@ import kotlinx.coroutines.launch
|
|||||||
class AppSessionViewModel @Inject constructor(
|
class AppSessionViewModel @Inject constructor(
|
||||||
private val coordinator: SessionCoordinator,
|
private val coordinator: SessionCoordinator,
|
||||||
private val profileStore: ProviderProfileStore,
|
private val profileStore: ProviderProfileStore,
|
||||||
private val availability: ProviderAvailability,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val bootstrapResult = MutableStateFlow<ProviderBootstrapResult?>(null)
|
private val bootstrapResult = MutableStateFlow<ProviderBootstrapResult?>(null)
|
||||||
|
|
||||||
@@ -50,18 +47,10 @@ class AppSessionViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val activeProfile = profileStore.activeProfile()
|
val activeProfile = profileStore.activeProfile()
|
||||||
if (activeProfile != null && availability.forType(activeProfile.type) is Availability.Disabled) {
|
bootstrapResult.value = ProviderBootstrapResult(
|
||||||
coordinator.switchProvider()
|
profileId = activeProfile?.id,
|
||||||
bootstrapResult.value = ProviderBootstrapResult(
|
result = coordinator.bootstrap(),
|
||||||
profileId = null,
|
)
|
||||||
result = DomainResult.Success(null),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
bootstrapResult.value = ProviderBootstrapResult(
|
|
||||||
profileId = activeProfile?.id,
|
|
||||||
result = coordinator.bootstrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error: kotlinx.coroutines.CancellationException) {
|
} catch (error: kotlinx.coroutines.CancellationException) {
|
||||||
throw error
|
throw error
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
@@ -110,7 +99,6 @@ class AppSessionViewModel @Inject constructor(
|
|||||||
bootstrapError: String?,
|
bootstrapError: String?,
|
||||||
): AppSessionUiState = when {
|
): AppSessionUiState = when {
|
||||||
profile == null -> AppSessionUiState.ProviderSelection
|
profile == null -> AppSessionUiState.ProviderSelection
|
||||||
availability.forType(profile.type) is Availability.Disabled -> AppSessionUiState.ProviderSelection
|
|
||||||
session?.profileId == profile.id -> AppSessionUiState.Authenticated(profile)
|
session?.profileId == profile.id -> AppSessionUiState.Authenticated(profile)
|
||||||
else -> AppSessionUiState.Login(profile, bootstrapError)
|
else -> AppSessionUiState.Login(profile, bootstrapError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,9 @@
|
|||||||
<string name="provider_selection_title">选择提供方</string>
|
<string name="provider_selection_title">选择提供方</string>
|
||||||
<string name="provider_selection_description">选择当前配置的连接方式。不同提供方之间的会话、凭据和本地数据相互隔离。</string>
|
<string name="provider_selection_description">选择当前配置的连接方式。不同提供方之间的会话、凭据和本地数据相互隔离。</string>
|
||||||
<string name="provider_abox_name">ABox</string>
|
<string name="provider_abox_name">ABox</string>
|
||||||
<string name="provider_abox_description">通过兼容 ABSDK 的 ABox 服务连接设备。</string>
|
<string name="provider_abox_description">通过 ABSDK 连接当前局域网中的 ABox 智能家居网关。</string>
|
||||||
<string name="provider_compatible_name">自有兼容后端</string>
|
|
||||||
<string name="provider_compatible_description">在协议准备完成后连接自行部署的 ABSDK 兼容服务。</string>
|
|
||||||
<string name="provider_local_name">本地体验</string>
|
<string name="provider_local_name">本地体验</string>
|
||||||
<string name="provider_local_description">无需网络,通过本地模拟设备体验完整应用流程。</string>
|
<string name="provider_local_description">无需网络,通过本地模拟设备体验应用。</string>
|
||||||
<string name="provider_status_available">可用</string>
|
|
||||||
<string name="provider_status_simulation">开发模拟</string>
|
|
||||||
<string name="provider_status_protocol_pending">协议待确认</string>
|
|
||||||
<string name="provider_status_transport_pending">等待传输安全方案批准</string>
|
|
||||||
<string name="login_title">登录 %1$s</string>
|
<string name="login_title">登录 %1$s</string>
|
||||||
<string name="login_provider_context">当前提供方:%1$s</string>
|
<string name="login_provider_context">当前提供方:%1$s</string>
|
||||||
<string name="login_back_to_providers">返回提供方选择</string>
|
<string name="login_back_to_providers">返回提供方选择</string>
|
||||||
|
|||||||
@@ -3,15 +3,9 @@
|
|||||||
<string name="provider_selection_title">Choose a provider</string>
|
<string name="provider_selection_title">Choose a provider</string>
|
||||||
<string name="provider_selection_description">Choose how this profile connects. Sessions, credentials, and local data stay isolated between providers.</string>
|
<string name="provider_selection_description">Choose how this profile connects. Sessions, credentials, and local data stay isolated between providers.</string>
|
||||||
<string name="provider_abox_name">ABox</string>
|
<string name="provider_abox_name">ABox</string>
|
||||||
<string name="provider_abox_description">Connect through the ABSDK-compatible ABox service.</string>
|
<string name="provider_abox_description">Connect to the ABox smart-home gateway through ABSDK on the current local network.</string>
|
||||||
<string name="provider_compatible_name">Compatible backend</string>
|
|
||||||
<string name="provider_compatible_description">Use a self-hosted ABSDK-compatible service when its protocol is ready.</string>
|
|
||||||
<string name="provider_local_name">Local experience</string>
|
<string name="provider_local_name">Local experience</string>
|
||||||
<string name="provider_local_description">Explore the complete app flow with local simulated devices and no network.</string>
|
<string name="provider_local_description">Explore the app with simulated local devices and no network connection.</string>
|
||||||
<string name="provider_status_available">Available</string>
|
|
||||||
<string name="provider_status_simulation">Development simulation</string>
|
|
||||||
<string name="provider_status_protocol_pending">Protocol pending</string>
|
|
||||||
<string name="provider_status_transport_pending">Transport security approval required</string>
|
|
||||||
<string name="login_title">Sign in to %1$s</string>
|
<string name="login_title">Sign in to %1$s</string>
|
||||||
<string name="login_provider_context">Selected provider: %1$s</string>
|
<string name="login_provider_context">Selected provider: %1$s</string>
|
||||||
<string name="login_back_to_providers">Back to providers</string>
|
<string name="login_back_to_providers">Back to providers</string>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.flagship.abox.manager.testing
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestDispatcher
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.rules.TestWatcher
|
||||||
|
import org.junit.runner.Description
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class MainDispatcherRule(
|
||||||
|
val dispatcher: TestDispatcher = StandardTestDispatcher(),
|
||||||
|
) : TestWatcher() {
|
||||||
|
override fun starting(description: Description) {
|
||||||
|
Dispatchers.setMain(dispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finished(description: Description) {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package com.flagship.abox.manager.ui.provider
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.ProviderType
|
||||||
|
import com.flagship.abox.manager.domain.SessionCoordinator
|
||||||
|
import com.flagship.abox.manager.testing.MainDispatcherRule
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeCredentialStore
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeLocalSessionGateway
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeProviderProfileStore
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeSessionGateway
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ProviderSelectionViewModelTest {
|
||||||
|
@get:Rule
|
||||||
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
|
private val coordinator = SessionCoordinator(
|
||||||
|
profileStore = FakeProviderProfileStore(),
|
||||||
|
credentialStore = FakeCredentialStore(),
|
||||||
|
remoteGateway = FakeSessionGateway(),
|
||||||
|
localGateway = FakeLocalSessionGateway(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exposesExactlyAboxAndLocalExperience() {
|
||||||
|
val viewModel = ProviderSelectionViewModel(coordinator)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(ProviderType.ABOX, ProviderType.LOCAL_EXPERIENCE),
|
||||||
|
viewModel.uiState.value.providers.map { it.type },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun selectingLocalExperienceEstablishesSessionImmediately() = runTest {
|
||||||
|
val viewModel = ProviderSelectionViewModel(coordinator)
|
||||||
|
val local = viewModel.uiState.value.providers
|
||||||
|
.single { it.type == ProviderType.LOCAL_EXPERIENCE }
|
||||||
|
|
||||||
|
viewModel.select(local)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(local.id, coordinator.observeActiveSession().first()?.profileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user