From a13a43d4312a7541f85e2e7b77cef2eb88a98565 Mon Sep 17 00:00:00 2001 From: Atsumi3 Date: Fri, 4 Sep 2026 18:13:32 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=AE=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC=E3=83=89=E3=81=AB?= =?UTF-8?q?=E3=82=88=E3=82=8B=E6=93=8D=E4=BD=9C=E5=88=B6=E9=99=90=E3=82=92?= =?UTF-8?q?=E5=BB=83=E6=AD=A2=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kidspos/api/DemoDangerZoneService.kt | 14 -- .../cuc/android/kidspos/di/hilt/ApiModule.kt | 6 - .../api/DangerZoneRateLimitedException.kt | 5 - .../android/kidspos/api/DangerZoneService.kt | 9 - .../kidspos/api/OpenApiDangerZoneService.kt | 43 ---- .../data/repository/DangerZoneRepository.kt | 23 -- .../kidspos/entity/DangerZoneVerification.kt | 7 - .../kidspos/ui/settings/SettingsScreen.kt | 128 ++---------- .../kidspos/ui/settings/SettingsViewModel.kt | 94 +-------- app/src/main/res/values/strings.xml | 14 -- .../cuc/android/kidspos/di/hilt/ApiModule.kt | 13 -- .../api/OpenApiDangerZoneServiceTest.kt | 131 ------------ .../kidspos/testutil/FakeDangerZoneService.kt | 21 -- .../android/kidspos/testutil/TestFactories.kt | 3 - .../cuc/android/kidspos/ui/ScreenshotTest.kt | 16 -- .../kidspos/ui/settings/SettingsScreenTest.kt | 136 ------------ .../ui/settings/SettingsViewModelTest.kt | 197 ------------------ 17 files changed, 18 insertions(+), 842 deletions(-) delete mode 100644 app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/api/DemoDangerZoneService.kt delete mode 100644 app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneRateLimitedException.kt delete mode 100644 app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneService.kt delete mode 100644 app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneService.kt delete mode 100644 app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/data/repository/DangerZoneRepository.kt delete mode 100644 app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/DangerZoneVerification.kt delete mode 100644 app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneServiceTest.kt delete mode 100644 app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/FakeDangerZoneService.kt diff --git a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/api/DemoDangerZoneService.kt b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/api/DemoDangerZoneService.kt deleted file mode 100644 index 54b73b5..0000000 --- a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/api/DemoDangerZoneService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.api - -import info.nukoneko.cuc.android.kidspos.entity.DangerZoneVerification - -class DemoDangerZoneService : DangerZoneService { - override suspend fun isPasswordConfigured(): Boolean = false - - override suspend fun verifyPassword(password: String): DangerZoneVerification = - DangerZoneVerification( - valid = false, - configured = false, - message = "デモモードではパスワードは設定されていません" - ) -} diff --git a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt index d968b89..c51edc0 100644 --- a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt +++ b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt @@ -6,10 +6,8 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import info.nukoneko.cuc.android.kidspos.api.APIService import info.nukoneko.cuc.android.kidspos.api.AppUpdateService -import info.nukoneko.cuc.android.kidspos.api.DangerZoneService import info.nukoneko.cuc.android.kidspos.api.DemoAPIService import info.nukoneko.cuc.android.kidspos.api.DemoAppUpdateService -import info.nukoneko.cuc.android.kidspos.api.DemoDangerZoneService import javax.inject.Singleton @Module @@ -23,8 +21,4 @@ object ApiModule { @Provides @Singleton fun provideAppUpdateService(): AppUpdateService = DemoAppUpdateService() - - @Provides - @Singleton - fun provideDangerZoneService(): DangerZoneService = DemoDangerZoneService() } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneRateLimitedException.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneRateLimitedException.kt deleted file mode 100644 index b988fe6..0000000 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneRateLimitedException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.api - -class DangerZoneRateLimitedException( - val retryAfterSeconds: Long? -) : Exception("Danger zone verification is rate limited") diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneService.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneService.kt deleted file mode 100644 index 928305b..0000000 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/DangerZoneService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.api - -import info.nukoneko.cuc.android.kidspos.entity.DangerZoneVerification - -interface DangerZoneService { - suspend fun isPasswordConfigured(): Boolean - - suspend fun verifyPassword(password: String): DangerZoneVerification -} diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneService.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneService.kt deleted file mode 100644 index a82d45f..0000000 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneService.kt +++ /dev/null @@ -1,43 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.api - -import info.nukoneko.cuc.android.kidspos.api.generated.DangerZoneApi -import info.nukoneko.cuc.android.kidspos.api.generated.model.VerifyDangerZonePasswordRequest -import info.nukoneko.cuc.android.kidspos.entity.DangerZoneVerification - -class OpenApiDangerZoneService( - private val dangerZoneApi: DangerZoneApi -) : DangerZoneService { - - override suspend fun isPasswordConfigured(): Boolean { - val response = dangerZoneApi.getDangerZoneStatus() - if (!response.isSuccessful) { - throw Exception("Failed to get danger zone status: ${response.code()}") - } - return response.body()!!.configured - } - - override suspend fun verifyPassword(password: String): DangerZoneVerification { - val response = dangerZoneApi.verifyDangerZonePassword( - VerifyDangerZonePasswordRequest(password = password) - ) - if (response.code() == HTTP_TOO_MANY_REQUESTS) { - throw DangerZoneRateLimitedException( - response.headers()[RETRY_AFTER_HEADER]?.trim()?.toLongOrNull() - ) - } - if (!response.isSuccessful) { - throw Exception("Failed to verify danger zone password: ${response.code()}") - } - val body = response.body()!! - return DangerZoneVerification( - valid = body.valid, - configured = body.configured, - message = body.message - ) - } - - private companion object { - const val HTTP_TOO_MANY_REQUESTS = 429 - const val RETRY_AFTER_HEADER = "Retry-After" - } -} diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/data/repository/DangerZoneRepository.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/data/repository/DangerZoneRepository.kt deleted file mode 100644 index 5449226..0000000 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/data/repository/DangerZoneRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.data.repository - -import info.nukoneko.cuc.android.kidspos.api.DangerZoneService -import info.nukoneko.cuc.android.kidspos.di.hilt.IoDispatcher -import info.nukoneko.cuc.android.kidspos.entity.DangerZoneVerification -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class DangerZoneRepository @Inject constructor( - private val dangerZoneService: DangerZoneService, - @IoDispatcher private val dispatcher: CoroutineDispatcher -) { - suspend fun isPasswordConfigured(): Boolean = withContext(dispatcher) { - dangerZoneService.isPasswordConfigured() - } - - suspend fun verifyPassword(password: String): DangerZoneVerification = withContext(dispatcher) { - dangerZoneService.verifyPassword(password) - } -} diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/DangerZoneVerification.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/DangerZoneVerification.kt deleted file mode 100644 index ee7d958..0000000 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/DangerZoneVerification.kt +++ /dev/null @@ -1,7 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.entity - -data class DangerZoneVerification( - val valid: Boolean, - val configured: Boolean, - val message: String -) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt index 6b92856..a2d1720 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -32,8 +31,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -88,8 +85,22 @@ fun SettingsScreen( readOnly = true, modifier = Modifier.fillMaxWidth() ) + Spacer(modifier = Modifier.padding(8.dp)) + Button( + onClick = { scanLauncher.launch(ScanOptions()) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.load_setting)) + } Spacer(modifier = Modifier.padding(16.dp)) Text(stringResource(R.string.current_mode_format, state.mode.modeName)) + Spacer(modifier = Modifier.padding(8.dp)) + Button( + onClick = viewModel::onToggleMode, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.switch_mode_format, state.mode.toggle().modeName)) + } Spacer(modifier = Modifier.padding(16.dp)) Text( stringResource( @@ -101,33 +112,10 @@ fun SettingsScreen( Spacer(modifier = Modifier.padding(16.dp)) HorizontalDivider() Spacer(modifier = Modifier.padding(16.dp)) - DangerZoneSection( + AppUpdateSection( state = state, - onPasswordChange = viewModel::onDangerZonePasswordChange, - onUnlock = viewModel::onUnlockDangerZone, - onLock = viewModel::onLockDangerZone + onCheckUpdate = viewModel::onCheckUpdate ) - if (state.dangerZoneUnlocked) { - Spacer(modifier = Modifier.padding(16.dp)) - Button( - onClick = { scanLauncher.launch(ScanOptions()) }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.load_setting)) - } - Spacer(modifier = Modifier.padding(8.dp)) - Button( - onClick = viewModel::onToggleMode, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.switch_mode_format, state.mode.toggle().modeName)) - } - Spacer(modifier = Modifier.padding(16.dp)) - AppUpdateSection( - state = state, - onCheckUpdate = viewModel::onCheckUpdate - ) - } } } @@ -141,90 +129,6 @@ fun SettingsScreen( } } -@Composable -private fun DangerZoneSection( - state: SettingsUiState, - onPasswordChange: (String) -> Unit, - onUnlock: () -> Unit, - onLock: () -> Unit -) { - Text(stringResource(R.string.danger_zone)) - Spacer(modifier = Modifier.padding(4.dp)) - Text(stringResource(R.string.danger_zone_description)) - - when (val status = state.dangerZoneStatus) { - is DangerZoneStatus.Checking -> { - Spacer(modifier = Modifier.padding(8.dp)) - Text(stringResource(R.string.danger_zone_checking)) - } - - is DangerZoneStatus.Locked -> { - Spacer(modifier = Modifier.padding(8.dp)) - OutlinedTextField( - value = state.dangerZonePassword, - onValueChange = onPasswordChange, - label = { Text(stringResource(R.string.danger_zone_password)) }, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.padding(4.dp)) - Button( - onClick = onUnlock, - enabled = state.dangerZonePassword.isNotEmpty() && !state.dangerZoneVerifying, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.danger_zone_unlock)) - } - if (state.dangerZoneVerifying) { - Spacer(modifier = Modifier.padding(4.dp)) - Text(stringResource(R.string.danger_zone_unlocking)) - } - when (val error = status.error) { - null -> Unit - is DangerZoneError.Rejected -> { - Spacer(modifier = Modifier.padding(4.dp)) - Text(error.message) - } - - is DangerZoneError.RateLimited -> { - Spacer(modifier = Modifier.padding(4.dp)) - val seconds = error.retryAfterSeconds - Text( - if (seconds != null) { - stringResource(R.string.danger_zone_rate_limited, seconds) - } else { - stringResource(R.string.danger_zone_rate_limited_unknown) - } - ) - } - - is DangerZoneError.Unreachable -> { - Spacer(modifier = Modifier.padding(4.dp)) - Text(stringResource(R.string.danger_zone_verify_failed)) - } - } - } - - is DangerZoneStatus.Unlocked -> { - val message = when (status.reason) { - DangerZoneReason.NOT_CONFIGURED -> R.string.danger_zone_not_configured - DangerZoneReason.STATUS_UNAVAILABLE -> R.string.danger_zone_status_failed - DangerZoneReason.VERIFIED -> R.string.danger_zone_unlocked - } - Spacer(modifier = Modifier.padding(8.dp)) - Text(stringResource(message)) - if (status.reason == DangerZoneReason.VERIFIED) { - Spacer(modifier = Modifier.padding(4.dp)) - TextButton(onClick = onLock) { - Text(stringResource(R.string.danger_zone_lock)) - } - } - } - } -} - @Composable private fun AppUpdateSection( state: SettingsUiState, diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModel.kt index d2f6272..3f761f6 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModel.kt @@ -4,8 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import info.nukoneko.cuc.android.kidspos.BuildConfig -import info.nukoneko.cuc.android.kidspos.api.DangerZoneRateLimitedException -import info.nukoneko.cuc.android.kidspos.data.repository.DangerZoneRepository import info.nukoneko.cuc.android.kidspos.data.settings.SettingsRepository import info.nukoneko.cuc.android.kidspos.update.AppUpdateManager import info.nukoneko.cuc.android.kidspos.update.UpdateStatus @@ -16,44 +14,19 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject -enum class DangerZoneReason { - NOT_CONFIGURED, - STATUS_UNAVAILABLE, - VERIFIED -} - -sealed interface DangerZoneStatus { - data object Checking : DangerZoneStatus - data class Locked(val error: DangerZoneError? = null) : DangerZoneStatus - data class Unlocked(val reason: DangerZoneReason) : DangerZoneStatus -} - -sealed interface DangerZoneError { - data class Rejected(val message: String) : DangerZoneError - data class RateLimited(val retryAfterSeconds: Long?) : DangerZoneError - data object Unreachable : DangerZoneError -} - data class SettingsUiState( val serverAddress: String = "", val mode: Mode = Mode.PRACTICE, val currentVersionName: String = BuildConfig.VERSION_NAME, val currentVersionCode: Int = BuildConfig.VERSION_CODE, - val updateStatus: UpdateStatus = UpdateStatus.Idle, - val dangerZoneStatus: DangerZoneStatus = DangerZoneStatus.Checking, - val dangerZonePassword: String = "", - val dangerZoneVerifying: Boolean = false -) { - val dangerZoneUnlocked: Boolean get() = dangerZoneStatus is DangerZoneStatus.Unlocked -} + val updateStatus: UpdateStatus = UpdateStatus.Idle +) @HiltViewModel class SettingsViewModel @Inject constructor( private val settingsRepository: SettingsRepository, - private val dangerZoneRepository: DangerZoneRepository, private val appUpdateManager: AppUpdateManager, private val applicationScope: CoroutineScope ) : ViewModel() { @@ -79,69 +52,6 @@ class SettingsViewModel @Inject constructor( _uiState.update { it.copy(updateStatus = status) } } } - checkDangerZoneStatus() - } - - private fun checkDangerZoneStatus() { - viewModelScope.launch { - val status = try { - if (dangerZoneRepository.isPasswordConfigured()) { - DangerZoneStatus.Locked() - } else { - DangerZoneStatus.Unlocked(DangerZoneReason.NOT_CONFIGURED) - } - } catch (e: Exception) { - Timber.w(e, "Failed to get danger zone status") - DangerZoneStatus.Unlocked(DangerZoneReason.STATUS_UNAVAILABLE) - } - _uiState.update { it.copy(dangerZoneStatus = status) } - } - } - - fun onDangerZonePasswordChange(value: String) { - _uiState.update { - it.copy(dangerZonePassword = value, dangerZoneStatus = DangerZoneStatus.Locked()) - } - } - - fun onUnlockDangerZone() { - val password = _uiState.value.dangerZonePassword - if (password.isEmpty() || _uiState.value.dangerZoneVerifying) return - _uiState.update { it.copy(dangerZoneVerifying = true) } - viewModelScope.launch { - val status = try { - val result = dangerZoneRepository.verifyPassword(password) - when { - result.valid -> DangerZoneStatus.Unlocked(DangerZoneReason.VERIFIED) - !result.configured -> DangerZoneStatus.Unlocked(DangerZoneReason.NOT_CONFIGURED) - else -> DangerZoneStatus.Locked(DangerZoneError.Rejected(result.message)) - } - } catch (e: DangerZoneRateLimitedException) { - Timber.w(e, "Danger zone verification is rate limited") - DangerZoneStatus.Locked(DangerZoneError.RateLimited(e.retryAfterSeconds)) - } catch (e: Exception) { - Timber.w(e, "Failed to verify danger zone password") - DangerZoneStatus.Locked(DangerZoneError.Unreachable) - } - _uiState.update { - it.copy( - dangerZoneStatus = status, - dangerZoneVerifying = false, - dangerZonePassword = if (status is DangerZoneStatus.Unlocked) { - "" - } else { - it.dangerZonePassword - } - ) - } - } - } - - fun onLockDangerZone() { - _uiState.update { - it.copy(dangerZoneStatus = DangerZoneStatus.Checking, dangerZonePassword = "") - } - checkDangerZoneStatus() } // 書き込み直後に画面を離れると viewModelScope が閉じて保存が取り消されるため、 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e2500dc..df797df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,18 +67,4 @@ アプリを更新するには、不明なアプリのインストールを許可してください 設定を開く - - Danger Zone - 接続先の変更・モードの切り替え・アプリの更新は、パスワードを入れてから操作できます - パスワードの設定状態を確認しています… - パスワード - ロックを解除する - 確認しています… - もう一度ロックする - ロックを解除しました - サーバーにパスワードが設定されていないため、そのまま操作できます - サーバーに接続できないため、そのまま操作できます - サーバーに接続できませんでした - 試行回数が多すぎます。%1$d秒後にもう一度お試しください - 試行回数が多すぎます。しばらく待ってからもう一度お試しください diff --git a/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt b/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt index 46a5246..256f5f1 100644 --- a/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt +++ b/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/hilt/ApiModule.kt @@ -6,12 +6,9 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import info.nukoneko.cuc.android.kidspos.api.APIService import info.nukoneko.cuc.android.kidspos.api.AppUpdateService -import info.nukoneko.cuc.android.kidspos.api.DangerZoneService import info.nukoneko.cuc.android.kidspos.api.OpenApiAPIService import info.nukoneko.cuc.android.kidspos.api.OpenApiAppUpdateService -import info.nukoneko.cuc.android.kidspos.api.OpenApiDangerZoneService import info.nukoneko.cuc.android.kidspos.api.generated.ApkApi -import info.nukoneko.cuc.android.kidspos.api.generated.DangerZoneApi import info.nukoneko.cuc.android.kidspos.api.generated.ItemsApi import info.nukoneko.cuc.android.kidspos.api.generated.SalesApi import info.nukoneko.cuc.android.kidspos.api.generated.StatusApi @@ -28,11 +25,6 @@ object ApiModule { fun provideApkApi(retrofit: Retrofit): ApkApi = retrofit.create(ApkApi::class.java) - @Provides - @Singleton - fun provideDangerZoneApi(retrofit: Retrofit): DangerZoneApi = - retrofit.create(DangerZoneApi::class.java) - @Provides @Singleton fun provideItemsApi(retrofit: Retrofit): ItemsApi = @@ -71,9 +63,4 @@ object ApiModule { @Singleton fun provideAppUpdateService(apkApi: ApkApi): AppUpdateService = OpenApiAppUpdateService(apkApi) - - @Provides - @Singleton - fun provideDangerZoneService(dangerZoneApi: DangerZoneApi): DangerZoneService = - OpenApiDangerZoneService(dangerZoneApi) } diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneServiceTest.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneServiceTest.kt deleted file mode 100644 index 34a861f..0000000 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/api/OpenApiDangerZoneServiceTest.kt +++ /dev/null @@ -1,131 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.api - -import info.nukoneko.cuc.android.kidspos.api.generated.DangerZoneApi -import info.nukoneko.cuc.android.kidspos.api.generated.model.ChangeDangerZonePasswordRequest -import info.nukoneko.cuc.android.kidspos.api.generated.model.ClearDangerZonePasswordRequest -import info.nukoneko.cuc.android.kidspos.api.generated.model.DangerZonePasswordResponse -import info.nukoneko.cuc.android.kidspos.api.generated.model.DangerZoneStatusResponse -import info.nukoneko.cuc.android.kidspos.api.generated.model.DangerZoneVerifyResponse -import info.nukoneko.cuc.android.kidspos.api.generated.model.VerifyDangerZonePasswordRequest -import kotlinx.coroutines.test.runTest -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.ResponseBody.Companion.toResponseBody -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test -import retrofit2.Response - -class OpenApiDangerZoneServiceTest { - - private class FakeDangerZoneApi( - private val verifyResponse: Response - ) : DangerZoneApi { - override suspend fun getDangerZoneStatus(): Response = - Response.success(DangerZoneStatusResponse(configured = true)) - - override suspend fun verifyDangerZonePassword( - verifyDangerZonePasswordRequest: VerifyDangerZonePasswordRequest - ): Response = verifyResponse - - override suspend fun changeDangerZonePassword( - changeDangerZonePasswordRequest: ChangeDangerZonePasswordRequest - ): Response = - Response.success( - DangerZonePasswordResponse( - success = true, - message = "パスワードを保存しました", - configured = true - ) - ) - - override suspend fun clearDangerZonePassword( - clearDangerZonePasswordRequest: ClearDangerZonePasswordRequest - ): Response = - Response.success( - DangerZonePasswordResponse( - success = true, - message = "パスワードを解除しました", - configured = false - ) - ) - } - - private fun tooManyRequests(retryAfter: String?): Response { - val raw = okhttp3.Response.Builder() - .request(Request.Builder().url("http://localhost/api/setting/danger-zone/verify").build()) - .protocol(Protocol.HTTP_1_1) - .code(429) - .message("Too Many Requests") - .apply { retryAfter?.let { header("Retry-After", it) } } - .build() - return Response.error("{}".toResponseBody("application/json".toMediaType()), raw) - } - - private fun serviceWith(response: Response) = - OpenApiDangerZoneService(FakeDangerZoneApi(response)) - - @Test - fun successfulVerifyIsMappedToVerification() = runTest { - val service = serviceWith( - Response.success( - DangerZoneVerifyResponse(valid = true, configured = true, message = "認証しました") - ) - ) - - val result = service.verifyPassword("secret") - - assertEquals(true, result.valid) - assertEquals(true, result.configured) - assertEquals("認証しました", result.message) - } - - @Test - fun tooManyRequestsRaisesRateLimitedWithRetryAfter() = runTest { - val service = serviceWith(tooManyRequests("45")) - - val error = runCatching { service.verifyPassword("wrong") }.exceptionOrNull() - - assertTrue(error is DangerZoneRateLimitedException) - assertEquals(45L, (error as DangerZoneRateLimitedException).retryAfterSeconds) - } - - @Test - fun tooManyRequestsWithoutRetryAfterRaisesRateLimitedWithoutSeconds() = runTest { - val service = serviceWith(tooManyRequests(null)) - - val error = runCatching { service.verifyPassword("wrong") }.exceptionOrNull() - - assertTrue(error is DangerZoneRateLimitedException) - assertNull((error as DangerZoneRateLimitedException).retryAfterSeconds) - } - - @Test - fun tooManyRequestsWithHttpDateRetryAfterRaisesRateLimitedWithoutSeconds() = runTest { - val service = serviceWith(tooManyRequests("Wed, 21 Oct 2015 07:28:00 GMT")) - - val error = runCatching { service.verifyPassword("wrong") }.exceptionOrNull() - - assertTrue(error is DangerZoneRateLimitedException) - assertNull((error as DangerZoneRateLimitedException).retryAfterSeconds) - } - - @Test - fun otherErrorStatusRaisesGenericFailure() = runTest { - val raw = okhttp3.Response.Builder() - .request(Request.Builder().url("http://localhost/api/setting/danger-zone/verify").build()) - .protocol(Protocol.HTTP_1_1) - .code(500) - .message("Internal Server Error") - .build() - val service = serviceWith( - Response.error("{}".toResponseBody("application/json".toMediaType()), raw) - ) - - val error = runCatching { service.verifyPassword("wrong") }.exceptionOrNull() - - assertTrue(error != null && error !is DangerZoneRateLimitedException) - } -} diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/FakeDangerZoneService.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/FakeDangerZoneService.kt deleted file mode 100644 index 943e6ba..0000000 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/FakeDangerZoneService.kt +++ /dev/null @@ -1,21 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.testutil - -import info.nukoneko.cuc.android.kidspos.api.DangerZoneService -import info.nukoneko.cuc.android.kidspos.entity.DangerZoneVerification - -class FakeDangerZoneService : DangerZoneService { - val verifiedPasswords = mutableListOf() - - var isPasswordConfiguredHandler: suspend () -> Boolean = { false } - - var verifyPasswordHandler: suspend (String) -> DangerZoneVerification = { - DangerZoneVerification(valid = false, configured = true, message = "パスワードが違います") - } - - override suspend fun isPasswordConfigured(): Boolean = isPasswordConfiguredHandler() - - override suspend fun verifyPassword(password: String): DangerZoneVerification { - verifiedPasswords += password - return verifyPasswordHandler(password) - } -} diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/TestFactories.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/TestFactories.kt index 6d65cd3..7705ce8 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/TestFactories.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/TestFactories.kt @@ -4,7 +4,6 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import info.nukoneko.cuc.android.kidspos.api.APIService import info.nukoneko.cuc.android.kidspos.data.repository.AppUpdateRepository -import info.nukoneko.cuc.android.kidspos.data.repository.DangerZoneRepository import info.nukoneko.cuc.android.kidspos.data.repository.ItemRepository import info.nukoneko.cuc.android.kidspos.data.repository.SaleRepository import info.nukoneko.cuc.android.kidspos.data.repository.ServerStatusRepository @@ -66,7 +65,6 @@ fun createSettingsViewModel( apkDownloader: FakeApkDownloader = FakeApkDownloader(), apkInstaller: FakeApkInstaller = FakeApkInstaller(), apkInstallResultBus: ApkInstallResultBus = ApkInstallResultBus(), - dangerZoneService: FakeDangerZoneService = FakeDangerZoneService(), dispatcher: CoroutineDispatcher = Dispatchers.Unconfined, appUpdateManager: AppUpdateManager = createAppUpdateManager( appUpdateService, @@ -78,7 +76,6 @@ fun createSettingsViewModel( applicationScope: CoroutineScope = CoroutineScope(dispatcher) ): SettingsViewModel = SettingsViewModel( settingsRepository, - DangerZoneRepository(dangerZoneService, dispatcher), appUpdateManager, applicationScope ) diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt index bcee1b6..8d835cd 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt @@ -16,7 +16,6 @@ import info.nukoneko.cuc.android.kidspos.entity.Store import info.nukoneko.cuc.android.kidspos.testutil.FakeAPIService import info.nukoneko.cuc.android.kidspos.testutil.FakeApkInstaller import info.nukoneko.cuc.android.kidspos.testutil.FakeAppUpdateService -import info.nukoneko.cuc.android.kidspos.testutil.FakeDangerZoneService import info.nukoneko.cuc.android.kidspos.testutil.MainDispatcherRule import info.nukoneko.cuc.android.kidspos.testutil.createMainViewModel import info.nukoneko.cuc.android.kidspos.testutil.createSettingsViewModel @@ -272,21 +271,6 @@ class ScreenshotTest { captureScreenRoboImage("screenshots/settings_screen.png") } - @Test - fun settingsScreenDangerZoneLocked() { - runBlocking { settingsRepository.setServerAddress("http://192.168.1.10:8080") } - val dangerZoneService = FakeDangerZoneService().apply { - isPasswordConfiguredHandler = { true } - } - setSettingsContent( - createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - ) - captureScreenRoboImage("screenshots/settings_screen_danger_zone_locked.png") - } - @Test fun settingsScreenUpdateAvailable() { val viewModel = createSettingsViewModel( diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt index 6b554fe..ec634ad 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt @@ -9,15 +9,11 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo -import androidx.compose.ui.test.performTextInput import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import info.nukoneko.cuc.android.kidspos.R -import info.nukoneko.cuc.android.kidspos.api.DangerZoneRateLimitedException import info.nukoneko.cuc.android.kidspos.entity.AppUpdate -import info.nukoneko.cuc.android.kidspos.entity.DangerZoneVerification import info.nukoneko.cuc.android.kidspos.testutil.FakeAppUpdateService -import info.nukoneko.cuc.android.kidspos.testutil.FakeDangerZoneService import info.nukoneko.cuc.android.kidspos.testutil.MainDispatcherRule import info.nukoneko.cuc.android.kidspos.testutil.createSettingsViewModel import info.nukoneko.cuc.android.kidspos.testutil.fakeSettingsRepository @@ -111,138 +107,6 @@ class SettingsScreenTest { composeRule.onNodeWithText(context.getString(R.string.app_is_up_to_date)).assertExists() } - @Test - fun lockedDangerZoneHidesDangerousActions() { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - composeRule.setContent { - SettingsScreen( - onNavigateBack = {}, - viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - ) - } - - composeRule.onNodeWithText(context.getString(R.string.danger_zone_unlock)) - .performScrollTo() - .assertIsDisplayed() - composeRule.onNodeWithText(context.getString(R.string.load_setting)).assertDoesNotExist() - composeRule.onNodeWithText(context.getString(R.string.check_update)).assertDoesNotExist() - composeRule.onNodeWithText( - context.getString(R.string.switch_mode_format, Mode.PRODUCTION.modeName) - ).assertDoesNotExist() - } - - @Test - fun correctPasswordRevealsDangerousActions() { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { - DangerZoneVerification(valid = true, configured = true, message = "OK") - } - composeRule.setContent { - SettingsScreen( - onNavigateBack = {}, - viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - ) - } - - composeRule.onNode(hasSetTextAction()).performTextInput("secret") - composeRule.onNodeWithText(context.getString(R.string.danger_zone_unlock)) - .performScrollTo() - .performClick() - composeRule.waitForIdle() - - assertEquals(listOf("secret"), dangerZoneService.verifiedPasswords) - composeRule.onNodeWithText(context.getString(R.string.load_setting)).assertExists() - composeRule.onNodeWithText(context.getString(R.string.check_update)).assertExists() - } - - @Test - fun wrongPasswordShowsServerMessage() { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { - DangerZoneVerification(valid = false, configured = true, message = "パスワードが違います") - } - composeRule.setContent { - SettingsScreen( - onNavigateBack = {}, - viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - ) - } - - composeRule.onNode(hasSetTextAction()).performTextInput("wrong") - composeRule.onNodeWithText(context.getString(R.string.danger_zone_unlock)) - .performScrollTo() - .performClick() - composeRule.waitForIdle() - - composeRule.onNodeWithText("パスワードが違います").assertExists() - composeRule.onNodeWithText(context.getString(R.string.load_setting)).assertDoesNotExist() - } - - @Test - fun rateLimitedVerifyShowsRetryAfterMessage() { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { throw DangerZoneRateLimitedException(45) } - composeRule.setContent { - SettingsScreen( - onNavigateBack = {}, - viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - ) - } - - composeRule.onNode(hasSetTextAction()).performTextInput("wrong") - composeRule.onNodeWithText(context.getString(R.string.danger_zone_unlock)) - .performScrollTo() - .performClick() - composeRule.waitForIdle() - - composeRule.onNodeWithText( - context.getString(R.string.danger_zone_rate_limited, 45L) - ).assertExists() - composeRule.onNodeWithText(context.getString(R.string.load_setting)).assertDoesNotExist() - } - - @Test - fun rateLimitedVerifyWithoutRetryAfterShowsGenericMessage() { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { throw DangerZoneRateLimitedException(null) } - composeRule.setContent { - SettingsScreen( - onNavigateBack = {}, - viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - ) - } - - composeRule.onNode(hasSetTextAction()).performTextInput("wrong") - composeRule.onNodeWithText(context.getString(R.string.danger_zone_unlock)) - .performScrollTo() - .performClick() - composeRule.waitForIdle() - - composeRule.onNodeWithText( - context.getString(R.string.danger_zone_rate_limited_unknown) - ).assertExists() - } - @Test fun availableUpdateShowsConfirmDialog() { val updateService = FakeAppUpdateService() diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModelTest.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModelTest.kt index 19ebcbe..4c463fd 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModelTest.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsViewModelTest.kt @@ -1,13 +1,10 @@ package info.nukoneko.cuc.android.kidspos.ui.settings import androidx.lifecycle.viewModelScope -import info.nukoneko.cuc.android.kidspos.api.DangerZoneRateLimitedException import info.nukoneko.cuc.android.kidspos.entity.AppUpdate -import info.nukoneko.cuc.android.kidspos.entity.DangerZoneVerification import info.nukoneko.cuc.android.kidspos.testutil.FakeApkDownloader import info.nukoneko.cuc.android.kidspos.testutil.FakeApkInstaller import info.nukoneko.cuc.android.kidspos.testutil.FakeAppUpdateService -import info.nukoneko.cuc.android.kidspos.testutil.FakeDangerZoneService import info.nukoneko.cuc.android.kidspos.testutil.MainDispatcherRule import info.nukoneko.cuc.android.kidspos.testutil.createAppUpdateManager import info.nukoneko.cuc.android.kidspos.testutil.createSettingsViewModel @@ -200,198 +197,4 @@ class SettingsViewModelTest { assertEquals(1, installer.installedApks.size) assertEquals(UpdateStatus.Installing, secondViewModel.uiState.value.updateStatus) } - - @Test - fun unconfiguredPasswordLeavesDangerZoneUnlocked() = runTest { - val viewModel = createSettingsViewModel(settingsRepository) - - assertEquals( - DangerZoneStatus.Unlocked(DangerZoneReason.NOT_CONFIGURED), - viewModel.uiState.value.dangerZoneStatus - ) - } - - @Test - fun configuredPasswordLocksDangerZone() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - assertEquals(DangerZoneStatus.Locked(), viewModel.uiState.value.dangerZoneStatus) - } - - @Test - fun statusFailureLeavesDangerZoneUnlocked() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { throw Exception("boom") } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - assertEquals( - DangerZoneStatus.Unlocked(DangerZoneReason.STATUS_UNAVAILABLE), - viewModel.uiState.value.dangerZoneStatus - ) - } - - @Test - fun correctPasswordUnlocksDangerZone() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { - DangerZoneVerification(valid = true, configured = true, message = "OK") - } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onDangerZonePasswordChange("secret") - viewModel.onUnlockDangerZone() - - assertEquals( - DangerZoneStatus.Unlocked(DangerZoneReason.VERIFIED), - viewModel.uiState.value.dangerZoneStatus - ) - assertEquals(listOf("secret"), dangerZoneService.verifiedPasswords) - assertEquals("", viewModel.uiState.value.dangerZonePassword) - } - - @Test - fun wrongPasswordKeepsDangerZoneLockedWithMessage() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onDangerZonePasswordChange("wrong") - viewModel.onUnlockDangerZone() - - assertEquals( - DangerZoneStatus.Locked(DangerZoneError.Rejected("パスワードが違います")), - viewModel.uiState.value.dangerZoneStatus - ) - assertEquals("wrong", viewModel.uiState.value.dangerZonePassword) - } - - @Test - fun verifyFailureKeepsDangerZoneLocked() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { throw Exception("boom") } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onDangerZonePasswordChange("secret") - viewModel.onUnlockDangerZone() - - assertEquals( - DangerZoneStatus.Locked(DangerZoneError.Unreachable), - viewModel.uiState.value.dangerZoneStatus - ) - } - - @Test - fun rateLimitedVerifyKeepsDangerZoneLockedWithRetryAfter() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { throw DangerZoneRateLimitedException(45) } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onDangerZonePasswordChange("wrong") - viewModel.onUnlockDangerZone() - - assertEquals( - DangerZoneStatus.Locked(DangerZoneError.RateLimited(45)), - viewModel.uiState.value.dangerZoneStatus - ) - assertEquals("wrong", viewModel.uiState.value.dangerZonePassword) - } - - @Test - fun rateLimitedVerifyWithoutRetryAfterKeepsDangerZoneLocked() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { throw DangerZoneRateLimitedException(null) } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onDangerZonePasswordChange("wrong") - viewModel.onUnlockDangerZone() - - assertEquals( - DangerZoneStatus.Locked(DangerZoneError.RateLimited(null)), - viewModel.uiState.value.dangerZoneStatus - ) - } - - @Test - fun passwordClearedOnServerUnlocksDangerZone() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { - DangerZoneVerification(valid = false, configured = false, message = "未設定です") - } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onDangerZonePasswordChange("secret") - viewModel.onUnlockDangerZone() - - assertEquals( - DangerZoneStatus.Unlocked(DangerZoneReason.NOT_CONFIGURED), - viewModel.uiState.value.dangerZoneStatus - ) - } - - @Test - fun emptyPasswordDoesNotCallServer() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onUnlockDangerZone() - - assertTrue(dangerZoneService.verifiedPasswords.isEmpty()) - assertEquals(DangerZoneStatus.Locked(), viewModel.uiState.value.dangerZoneStatus) - } - - @Test - fun lockDangerZoneRechecksServerStatus() = runTest { - val dangerZoneService = FakeDangerZoneService() - dangerZoneService.isPasswordConfiguredHandler = { true } - dangerZoneService.verifyPasswordHandler = { - DangerZoneVerification(valid = true, configured = true, message = "OK") - } - val viewModel = createSettingsViewModel( - settingsRepository, - dangerZoneService = dangerZoneService - ) - - viewModel.onDangerZonePasswordChange("secret") - viewModel.onUnlockDangerZone() - assertTrue(viewModel.uiState.value.dangerZoneUnlocked) - - viewModel.onLockDangerZone() - - assertEquals(DangerZoneStatus.Locked(), viewModel.uiState.value.dangerZoneStatus) - } } From 9cce4d8937c40ba1f125cbc67e4b48d6f1407b8d Mon Sep 17 00:00:00 2001 From: Atsumi3 Date: Fri, 4 Sep 2026 18:35:39 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=82=92=E6=A8=AA=E5=90=91=E3=81=8D=E5=90=91=E3=81=91=E3=81=AE?= =?UTF-8?q?=203=20=E3=82=AB=E3=83=BC=E3=83=89=E6=A7=8B=E6=88=90=E3=81=AB?= =?UTF-8?q?=E6=94=B9=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kidspos/ui/settings/SettingsScreen.kt | 196 ++++++++++++------ .../cuc/android/kidspos/ui/theme/Color.kt | 5 + .../cuc/android/kidspos/ui/theme/Theme.kt | 7 +- app/src/main/res/values/strings.xml | 2 - .../kidspos/ui/settings/SettingsScreenTest.kt | 21 +- 5 files changed, 164 insertions(+), 67 deletions(-) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt index a2d1720..0acfee5 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt @@ -6,27 +6,37 @@ import android.os.Build import android.provider.Settings import android.webkit.URLUtil import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -39,6 +49,7 @@ import com.journeyapps.barcodescanner.ScanOptions import info.nukoneko.cuc.android.kidspos.R import info.nukoneko.cuc.android.kidspos.update.UpdateFailure import info.nukoneko.cuc.android.kidspos.update.UpdateStatus +import info.nukoneko.cuc.android.kidspos.util.Mode import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @@ -71,50 +82,29 @@ fun SettingsScreen( ) } ) { innerPadding -> - Column( + Row( modifier = Modifier .fillMaxSize() .padding(innerPadding) .verticalScroll(rememberScrollState()) - .padding(16.dp) + .padding(24.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Top ) { - Text(stringResource(R.string.current_connetion_target)) - OutlinedTextField( - value = state.serverAddress, - onValueChange = viewModel::onServerAddressChange, - readOnly = true, - modifier = Modifier.fillMaxWidth() + ConnectionCard( + serverAddress = state.serverAddress, + onLoadSetting = { scanLauncher.launch(ScanOptions()) }, + modifier = Modifier.weight(1f) ) - Spacer(modifier = Modifier.padding(8.dp)) - Button( - onClick = { scanLauncher.launch(ScanOptions()) }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.load_setting)) - } - Spacer(modifier = Modifier.padding(16.dp)) - Text(stringResource(R.string.current_mode_format, state.mode.modeName)) - Spacer(modifier = Modifier.padding(8.dp)) - Button( - onClick = viewModel::onToggleMode, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.switch_mode_format, state.mode.toggle().modeName)) - } - Spacer(modifier = Modifier.padding(16.dp)) - Text( - stringResource( - R.string.current_version_format, - state.currentVersionName, - state.currentVersionCode - ) + ModeCard( + currentMode = state.mode, + onToggleMode = viewModel::onToggleMode, + modifier = Modifier.weight(1f) ) - Spacer(modifier = Modifier.padding(16.dp)) - HorizontalDivider() - Spacer(modifier = Modifier.padding(16.dp)) - AppUpdateSection( + AppUpdateCard( state = state, - onCheckUpdate = viewModel::onCheckUpdate + onCheckUpdate = viewModel::onCheckUpdate, + modifier = Modifier.weight(1f) ) } } @@ -129,6 +119,99 @@ fun SettingsScreen( } } +@Composable +private fun SettingsCard( + title: String, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + Card( + modifier = modifier, + shape = MaterialTheme.shapes.extraLarge, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) + ) { + Column( + modifier = Modifier.padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text(text = title, style = MaterialTheme.typography.titleLarge) + content() + } + } +} + +@Composable +private fun ActionButton( + text: String, + onClick: () -> Unit, + enabled: Boolean = true +) { + Button( + onClick = onClick, + enabled = enabled, + modifier = Modifier.heightIn(min = 56.dp), + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp) + ) { + Text(text = text, style = MaterialTheme.typography.titleMedium) + } +} + +@Composable +private fun ConnectionCard( + serverAddress: String, + onLoadSetting: () -> Unit, + modifier: Modifier = Modifier +) { + SettingsCard( + title = stringResource(R.string.current_connetion_target), + modifier = modifier + ) { + Text(text = serverAddress, style = MaterialTheme.typography.bodyLarge) + ActionButton(text = stringResource(R.string.load_setting), onClick = onLoadSetting) + } +} + +@Composable +private fun ModeCard( + currentMode: Mode, + onToggleMode: () -> Unit, + modifier: Modifier = Modifier +) { + SettingsCard( + title = stringResource(R.string.current_mode), + modifier = modifier + ) { + val modes = listOf(Mode.PRACTICE, Mode.PRODUCTION) + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + modes.forEachIndexed { index, mode -> + SegmentedButton( + selected = currentMode == mode, + onClick = { if (currentMode != mode) onToggleMode() }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = modes.size), + modifier = Modifier.heightIn(min = 56.dp), + label = { Text(mode.modeName) } + ) + } + } + } +} + +@Composable +private fun AppUpdateCard( + state: SettingsUiState, + onCheckUpdate: () -> Unit, + modifier: Modifier = Modifier +) { + SettingsCard( + title = stringResource(R.string.app_update), + modifier = modifier + ) { + AppUpdateSection(state = state, onCheckUpdate = onCheckUpdate) + } +} + @Composable private fun AppUpdateSection( state: SettingsUiState, @@ -137,33 +220,34 @@ private fun AppUpdateSection( val context = LocalContext.current val status = state.updateStatus - Text(stringResource(R.string.app_update)) - Spacer(modifier = Modifier.padding(8.dp)) - Button( + Text( + text = stringResource( + R.string.current_version_format, + state.currentVersionName, + state.currentVersionCode + ), + style = MaterialTheme.typography.bodyLarge + ) + ActionButton( + text = stringResource(R.string.check_update), onClick = onCheckUpdate, enabled = status !is UpdateStatus.Checking && status !is UpdateStatus.Downloading && - status !is UpdateStatus.Installing, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.check_update)) - } + status !is UpdateStatus.Installing + ) when (status) { is UpdateStatus.Idle, is UpdateStatus.Available -> Unit is UpdateStatus.Checking -> { - Spacer(modifier = Modifier.padding(8.dp)) Text(stringResource(R.string.checking_update)) } is UpdateStatus.UpToDate -> { - Spacer(modifier = Modifier.padding(8.dp)) Text(stringResource(R.string.app_is_up_to_date)) } is UpdateStatus.Downloading -> { - Spacer(modifier = Modifier.padding(8.dp)) Text( stringResource( R.string.downloading_update_format, @@ -178,15 +262,13 @@ private fun AppUpdateSection( } is UpdateStatus.Installing -> { - Spacer(modifier = Modifier.padding(8.dp)) Text(stringResource(R.string.installing_update)) } is UpdateStatus.InstallNotPermitted -> { - Spacer(modifier = Modifier.padding(8.dp)) Text(stringResource(R.string.install_permission_required)) - Spacer(modifier = Modifier.padding(4.dp)) - Button( + ActionButton( + text = stringResource(R.string.open_install_permission_setting), onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startActivity( @@ -196,15 +278,11 @@ private fun AppUpdateSection( ) ) } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.open_install_permission_setting)) - } + } + ) } is UpdateStatus.Failed -> { - Spacer(modifier = Modifier.padding(8.dp)) Text( when (status.cause) { UpdateFailure.CHECK -> stringResource(R.string.update_check_failed) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt index 26bec81..13a7331 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt @@ -12,3 +12,8 @@ val Surface = Color(0xFFFFFFFF) val OnSurface = Color(0xFF1C1C1C) val Error = Color(0xFFB00020) val OnError = Color(0xFFFFFFFF) +val SurfaceContainerLowest = Color(0xFFFFFFFF) +val SurfaceContainerLow = Color(0xFFF5F5F5) +val SurfaceContainer = Color(0xFFEEEEEE) +val SurfaceContainerHigh = Color(0xFFE8E8E8) +val SurfaceContainerHighest = Color(0xFFE0E0E0) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt index 6ac26e1..631bf88 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt @@ -14,7 +14,12 @@ private val LightColors = lightColorScheme( surface = Surface, onSurface = OnSurface, error = Error, - onError = OnError + onError = OnError, + surfaceContainerLowest = SurfaceContainerLowest, + surfaceContainerLow = SurfaceContainerLow, + surfaceContainer = SurfaceContainer, + surfaceContainerHigh = SurfaceContainerHigh, + surfaceContainerHighest = SurfaceContainerHighest ) @Composable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df797df..311af0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,8 +46,6 @@ [テストモード] 練習モードのためレシートは出ません 通信エラーが発生しました。 - 現在は %s モードです - %s に切り替える アプリの更新 diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt index ec634ad..2361f00 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt @@ -41,7 +41,7 @@ class SettingsScreenTest { private val context: Context = ApplicationProvider.getApplicationContext() @Test - fun currentServerAddressIsShownInTextField() { + fun currentServerAddressIsShown() { val viewModel = createSettingsViewModel(settingsRepository) composeRule.setContent { SettingsScreen(onNavigateBack = {}, viewModel = viewModel) @@ -61,20 +61,31 @@ class SettingsScreenTest { } @Test - fun toggleModeButtonSwitchesToProduction() { + fun selectingProductionSegmentSwitchesMode() { val viewModel = createSettingsViewModel(settingsRepository) composeRule.setContent { SettingsScreen(onNavigateBack = {}, viewModel = viewModel) } - composeRule.onNodeWithText( - context.getString(R.string.switch_mode_format, Mode.PRODUCTION.modeName) - ).performScrollTo().performClick() + composeRule.onNodeWithText(Mode.PRODUCTION.modeName).performScrollTo().performClick() composeRule.waitForIdle() assertEquals(Mode.PRODUCTION, viewModel.uiState.value.mode) } + @Test + fun selectingCurrentModeSegmentDoesNothing() { + val viewModel = createSettingsViewModel(settingsRepository) + composeRule.setContent { + SettingsScreen(onNavigateBack = {}, viewModel = viewModel) + } + + composeRule.onNodeWithText(Mode.PRACTICE.modeName).performScrollTo().performClick() + composeRule.waitForIdle() + + assertEquals(Mode.PRACTICE, viewModel.uiState.value.mode) + } + @Test fun backButtonInvokesNavigateBack() { var navigatedBack = false From 06d301d42dceecda7c86565e93e473e809b178b4 Mon Sep 17 00:00:00 2001 From: Atsumi3 Date: Fri, 4 Sep 2026 18:47:17 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E3=82=BF=E3=83=96?= =?UTF-8?q?=E3=83=AC=E3=83=83=E3=83=88=E6=A8=AA=E5=90=91=E3=81=8D=E3=81=AE?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E8=A8=AD=E5=AE=9A=E3=81=A7=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cuc/android/kidspos/ui/settings/SettingsScreenTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt index 2361f00..3468cc3 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreenTest.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import info.nukoneko.cuc.android.kidspos.R import info.nukoneko.cuc.android.kidspos.entity.AppUpdate import info.nukoneko.cuc.android.kidspos.testutil.FakeAppUpdateService @@ -28,7 +29,7 @@ import org.robolectric.annotation.Config // Robolectric の SDK 36 実行は JDK 21 が必要なため、CI の JDK 17 で動く SDK 35 に固定する @RunWith(AndroidJUnit4::class) -@Config(sdk = [35]) +@Config(sdk = [35], qualifiers = RobolectricDeviceQualifiers.MediumTablet) class SettingsScreenTest { private val mainDispatcherRule = MainDispatcherRule() private val composeRule = createComposeRule() From 113e0dd55420efe6c72ca24af898c711007c4403 Mon Sep 17 00:00:00 2001 From: Atsumi3 Date: Fri, 4 Sep 2026 18:51:22 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=E3=83=93=E3=83=AB=E3=83=89=E6=99=82?= =?UTF-8?q?=E3=81=AE=E3=82=B3=E3=83=B3=E3=83=91=E3=82=A4=E3=83=A9=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=E3=82=92=E8=A7=A3=E6=B6=88=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +++- .../info/nukoneko/cuc/android/kidspos/ui/main/MainScreen.kt | 2 +- .../cuc/android/kidspos/ui/settings/SettingsScreen.kt | 2 +- .../cuc/android/kidspos/update/ApkInstallResultBus.kt | 2 ++ .../cuc/android/kidspos/testutil/MainDispatcherRule.kt | 2 ++ .../info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt | 2 ++ gradle/libs.versions.toml | 1 + 7 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 594d998..b2aa700 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -120,6 +120,8 @@ android { kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) + optIn.add("kotlinx.serialization.ExperimentalSerializationApi") + freeCompilerArgs.add("-Xannotation-default-target=param-property") } } @@ -131,6 +133,7 @@ dependencies { implementation(libs.dagger.hilt.android) ksp(libs.dagger.hilt.android.compiler) implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.hilt.lifecycle.viewmodel.compose) implementation(platform(libs.compose.bom)) implementation(libs.compose.material3) @@ -187,7 +190,6 @@ openApiGenerate { outputDir.set(layout.buildDirectory.dir("generated/openapi").get().asFile.absolutePath) apiPackage.set("info.nukoneko.cuc.android.kidspos.api.generated") modelPackage.set("info.nukoneko.cuc.android.kidspos.api.generated.model") - invokerPackage.set("info.nukoneko.cuc.android.kidspos.api.generated.invoker") configOptions.set( mapOf( "library" to "jvm-retrofit2", diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainScreen.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainScreen.kt index 7fb9a62..d1e1b63 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainScreen.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainScreen.kt @@ -63,7 +63,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import info.nukoneko.cuc.android.kidspos.R import info.nukoneko.cuc.android.kidspos.entity.Item diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt index 0acfee5..ff12b3e 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/settings/SettingsScreen.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/update/ApkInstallResultBus.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/update/ApkInstallResultBus.kt index 6c58244..76d1552 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/update/ApkInstallResultBus.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/update/ApkInstallResultBus.kt @@ -1,5 +1,6 @@ package info.nukoneko.cuc.android.kidspos.update +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -21,6 +22,7 @@ class ApkInstallResultBus @Inject constructor() { _results.tryEmit(result) } + @OptIn(ExperimentalCoroutinesApi::class) fun clear() { _results.resetReplayCache() } diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/MainDispatcherRule.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/MainDispatcherRule.kt index 32bcd49..b9abd4d 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/MainDispatcherRule.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/testutil/MainDispatcherRule.kt @@ -1,6 +1,7 @@ package info.nukoneko.cuc.android.kidspos.testutil import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain @@ -8,6 +9,7 @@ import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher import org.junit.runner.Description +@OptIn(ExperimentalCoroutinesApi::class) class MainDispatcherRule( val dispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : TestWatcher() { diff --git a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt index 8d835cd..51cb8f2 100644 --- a/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt +++ b/app/src/test/kotlin/info/nukoneko/cuc/android/kidspos/ui/ScreenshotTest.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.captureScreenRoboImage import info.nukoneko.cuc.android.kidspos.R @@ -43,6 +44,7 @@ import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(sdk = [35], qualifiers = RobolectricDeviceQualifiers.MediumTablet) +@OptIn(ExperimentalRoborazziApi::class) class ScreenshotTest { private val mainDispatcherRule = MainDispatcherRule() private val composeRule = createComposeRule() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b08eef4..ce4042e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,6 +65,7 @@ android-material = { module = "com.google.android.material:material", version.re dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } dagger-hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" } +androidx-hilt-lifecycle-viewmodel-compose = { module = "androidx.hilt:hilt-lifecycle-viewmodel-compose", version.ref = "androidx-hilt" } okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" } okhttp-client = { module = "com.squareup.okhttp3:okhttp" } From 2880028cdd13d5c9c117cce5a825166e8756ca29 Mon Sep 17 00:00:00 2001 From: Atsumi3 Date: Fri, 4 Sep 2026 19:05:52 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=E9=81=B8=E6=8A=9E=E7=8A=B6=E6=85=8B?= =?UTF-8?q?=E3=81=AE=E9=9D=A2=E3=81=AE=E8=89=B2=E3=82=92=E3=83=8B=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=83=A9=E3=83=AB=E3=82=B0=E3=83=AC=E3=83=BC?= =?UTF-8?q?=E3=81=A7=E5=AE=9A=E7=BE=A9=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt | 2 ++ .../kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt index 13a7331..f75d42b 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Color.kt @@ -6,6 +6,8 @@ val Primary = Color(0xFFD32F2F) val OnPrimary = Color(0xFFFFFFFF) val Secondary = Color(0xFF455A64) val OnSecondary = Color(0xFFFFFFFF) +val SecondaryContainer = Color(0xFFE0E0E0) +val OnSecondaryContainer = Color(0xFF1C1C1C) val Background = Color(0xFFFAFAFA) val OnBackground = Color(0xFF1C1C1C) val Surface = Color(0xFFFFFFFF) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt index 631bf88..e468a03 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/theme/Theme.kt @@ -9,6 +9,8 @@ private val LightColors = lightColorScheme( onPrimary = OnPrimary, secondary = Secondary, onSecondary = OnSecondary, + secondaryContainer = SecondaryContainer, + onSecondaryContainer = OnSecondaryContainer, background = Background, onBackground = OnBackground, surface = Surface,