From 64446eff91a235bd71fcebb7d533509047f3b53e Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Sat, 28 Mar 2026 22:12:48 +0100 Subject: [PATCH 01/11] Update verification-metadata.xml --- gradle/verification-metadata.xml | 3267 +++++++++++++++--------------- 1 file changed, 1635 insertions(+), 1632 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 96f92386..43d519d4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6,15 +6,15 @@ + + + - - - @@ -22,51 +22,51 @@ + + + - - - + + + - - - - - - - - - + + + + + + + + + - - - @@ -79,12 +79,12 @@ - - - + + + @@ -97,12 +97,12 @@ - - - + + + @@ -110,26 +110,26 @@ + + + - - - + + + - - - @@ -140,138 +140,138 @@ - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - - - - + + + + + + - - - @@ -279,39 +279,39 @@ - - - + + + - - - + + + + + + - - - - - - + + + @@ -319,23 +319,23 @@ - - - + + + + + + - - - @@ -348,18 +348,18 @@ - - - - - - + + + + + + @@ -367,18 +367,18 @@ - - - - - - + + + + + + @@ -386,18 +386,18 @@ - - - - - - + + + + + + @@ -405,18 +405,18 @@ - - - - - - + + + + + + @@ -424,18 +424,18 @@ - - - - - - + + + + + + @@ -443,18 +443,18 @@ - - - - - - + + + + + + @@ -470,15 +470,15 @@ + + + - - - @@ -494,15 +494,15 @@ + + + - - - @@ -510,18 +510,18 @@ - - - - - - + + + + + + @@ -529,18 +529,18 @@ - - - - - - + + + + + + @@ -548,18 +548,18 @@ - - - - - - + + + + + + @@ -567,18 +567,18 @@ - - - - - - + + + + + + @@ -586,18 +586,18 @@ - - - - - - + + + + + + @@ -605,18 +605,18 @@ - - - - - - + + + + + + @@ -624,15 +624,15 @@ + + + - - - @@ -640,18 +640,18 @@ - - - - - - + + + + + + @@ -659,18 +659,18 @@ - - - - - - + + + + + + @@ -678,26 +678,26 @@ + + + - - - + + + - - - @@ -705,18 +705,18 @@ - - - - - - + + + + + + @@ -724,18 +724,18 @@ - - - - - - + + + + + + @@ -743,15 +743,15 @@ + + + - - - @@ -759,15 +759,15 @@ + + + - - - @@ -775,18 +775,18 @@ - - - - - - + + + + + + @@ -794,276 +794,276 @@ + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - @@ -1071,26 +1071,26 @@ + + + - - - + + + - - - @@ -1098,48 +1098,48 @@ + + + - - - + + + - - - + + + - - - + + + - - - @@ -1152,64 +1152,64 @@ - - - + + + + + + - - - - - - + + + + + + - - - + + + - - - + + + - - - @@ -1220,15 +1220,15 @@ + + + - - - @@ -1236,56 +1236,56 @@ - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - @@ -1293,23 +1293,23 @@ - - - + + + + + + - - - @@ -1317,42 +1317,42 @@ - - - + + + + + + - - - - - - + + + + + + - - - @@ -1360,18 +1360,18 @@ - - - - - - + + + + + + @@ -1379,15 +1379,15 @@ + + + - - - @@ -1403,42 +1403,42 @@ - - - + + + + + + - - - - - - + + + + + + - - - @@ -1446,29 +1446,29 @@ - - - - - - + + + + + + + + + - - - @@ -1481,70 +1481,70 @@ - - - + + + + + + - - - - - - - - - + + + + + + + + + - - - + + + - - - + + + - - - @@ -1552,18 +1552,18 @@ - - - - - - + + + + + + @@ -1571,114 +1571,114 @@ - - - - - - + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - @@ -1686,18 +1686,18 @@ - - - - - - + + + + + + @@ -1715,37 +1715,37 @@ - - - + + + - - - - - - + + + + + + - - - + + + @@ -1753,64 +1753,64 @@ - - - + + + + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - @@ -1818,18 +1818,18 @@ - - - - - - + + + + + + @@ -1837,217 +1837,217 @@ - - - - - - + + + + + + + + + - - - - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + - - + + + + + + + + + + + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - + + + - - - - - - + + + + + + @@ -2055,18 +2055,18 @@ - - - - - - + + + + + + @@ -2074,15 +2074,15 @@ + + + - - - @@ -2106,40 +2106,40 @@ - - - - - - + + + + + + + + + - - - + + + - - - @@ -2147,48 +2147,48 @@ + + + - - - + + + - - - + + + - - - + + + - - - @@ -2199,166 +2199,169 @@ + + + - - - + + + - - - - + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -2369,103 +2372,103 @@ + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -2492,15 +2495,15 @@ + + + - - - @@ -2535,235 +2538,235 @@ + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -2779,85 +2782,85 @@ + + + - - - - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -2868,26 +2871,26 @@ + + + - - - + + + - - - @@ -2895,15 +2898,15 @@ + + + - - - @@ -2911,40 +2914,40 @@ + + + - - - - - - - - - + + + + + + + + + - - - @@ -2957,18 +2960,18 @@ - - - - - - + + + + + + @@ -2979,32 +2982,32 @@ - - - - - - - - - - + + - - + + + + + + + + + + @@ -3022,62 +3025,62 @@ + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - + + + + + + @@ -3104,43 +3107,43 @@ - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + @@ -3151,18 +3154,18 @@ - - - - - - + + + + + + @@ -3196,50 +3199,50 @@ + + + - - - + + + - - - + + + - - - - - - - - - - - + + + + + + + + @@ -3256,54 +3259,54 @@ - - - + + + - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -3336,15 +3339,15 @@ + + + - - - @@ -3360,46 +3363,46 @@ - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -3418,15 +3421,15 @@ + + + - - - @@ -3467,18 +3470,18 @@ - - - - - - + + + + + + @@ -3497,46 +3500,46 @@ - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -3564,18 +3567,18 @@ - - - - - - + + + + + + @@ -3583,15 +3586,15 @@ + + + - - - @@ -3602,37 +3605,37 @@ + + + - - - + + + - - - + + + - - - @@ -3655,70 +3658,70 @@ + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -3734,29 +3737,29 @@ + + + - - - - - - - - - + + + + + + @@ -3764,46 +3767,46 @@ - - - - - - - - - - + + - - + + + + + + + + + + - - - - - - + + + + + + @@ -3926,40 +3929,40 @@ + + + - - - - - - - - - + + + + + + + + + - - - @@ -3975,15 +3978,15 @@ + + + - - - @@ -3996,18 +3999,18 @@ - - - - - - + + + + + + @@ -4030,18 +4033,18 @@ - - - - - - + + + + + + @@ -4049,40 +4052,40 @@ + + + - - - + + + - - - - - - - - - + + + + + + @@ -4109,15 +4112,15 @@ + + + - - - @@ -4314,14 +4317,19 @@ + + + - - + + + + @@ -4330,15 +4338,15 @@ + + + - - - @@ -4346,15 +4354,15 @@ + + + - - - @@ -4362,15 +4370,15 @@ + + + - - - @@ -4378,15 +4386,15 @@ + + + - - - @@ -4444,15 +4452,15 @@ + + + - - - @@ -4468,15 +4476,15 @@ + + + - - - @@ -4487,15 +4495,15 @@ + + + - - - @@ -4506,15 +4514,15 @@ + + + - - - @@ -4525,15 +4533,15 @@ + + + - - - @@ -4544,26 +4552,26 @@ + + + - - - + + + - - - @@ -4574,26 +4582,26 @@ + + + - - - + + + - - - @@ -4619,26 +4627,26 @@ + + + - - - + + + - - - @@ -4654,15 +4662,15 @@ + + + - - - @@ -4670,37 +4678,37 @@ + + + - - - + + + - - - + + + - - - @@ -4711,15 +4719,15 @@ + + + - - - @@ -4730,15 +4738,15 @@ + + + - - - @@ -4749,15 +4757,15 @@ + + + - - - @@ -4768,15 +4776,15 @@ + + + - - - @@ -4787,15 +4795,15 @@ + + + - - - @@ -4806,14 +4814,14 @@ + + + - - - - + @@ -4825,15 +4833,15 @@ + + + - - - @@ -4854,15 +4862,15 @@ + + + - - - @@ -4873,15 +4881,15 @@ + + + - - - @@ -4892,15 +4900,15 @@ + + + - - - @@ -4911,26 +4919,26 @@ + + + - - - + + + - - - @@ -4941,57 +4949,57 @@ + + + - - - - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -4999,74 +5007,74 @@ - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -5079,32 +5087,32 @@ - - - - - - - - - - + + - - + + + + + + + + + + @@ -5115,29 +5123,29 @@ + + + - - - - - - - - - + + + + + + @@ -5170,26 +5178,26 @@ + + + - - - + + + - - - @@ -5212,29 +5220,29 @@ + + + - - - - - - - - - + + + + + + @@ -5262,99 +5270,99 @@ - - - - - - - - - - + + - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -5365,15 +5373,15 @@ - - - + + + @@ -5392,29 +5400,29 @@ - - - - - - + + + + + + + + + - - - @@ -5425,15 +5433,15 @@ + + + - - - @@ -5456,18 +5464,18 @@ - - - - - - + + + + + + @@ -5503,74 +5511,74 @@ - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -5578,26 +5586,26 @@ + + + - - - + + + - - - @@ -5605,26 +5613,26 @@ + + + - - - + + + - - - @@ -5635,91 +5643,91 @@ + + + - - - - - - - - - - - - - + + - - + + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + @@ -5757,12 +5765,12 @@ - - - + + + @@ -5813,29 +5821,29 @@ + + + - - - - - - - - - + + + + + + @@ -5845,19 +5853,19 @@ - - - - - - - + + + + + + + @@ -5865,59 +5873,59 @@ + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -5936,26 +5944,26 @@ + + + - - - + + + - - - @@ -5966,15 +5974,15 @@ + + + - - - @@ -5985,76 +5993,76 @@ + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - + + + - - - @@ -6065,15 +6073,15 @@ + + + - - - @@ -6084,15 +6092,15 @@ + + + - - - @@ -6111,43 +6119,43 @@ + + + - - - + + + + + + - + + + - - + + - - - - - - - - @@ -6166,15 +6174,15 @@ + + + - - - @@ -6201,59 +6209,59 @@ + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -6292,154 +6300,154 @@ - - - - - - + + + + + + + + + - - - + + + + + + - + + + - + - - - - - - - - - - - - - - + + + + + + + + + - - - + + + + + + - + + + - + - - - - - - - - - + + + + - - - + + + - - - + + + - - - + + + - - - @@ -6447,34 +6455,34 @@ - - - + + + + + + - - - + + + - - - @@ -6510,12 +6518,12 @@ - - - + + + @@ -6528,34 +6536,34 @@ - - - + + + + + + - - - + + + - - - @@ -6577,15 +6585,15 @@ + + + - - - @@ -6593,15 +6601,15 @@ + + + - - - @@ -6627,26 +6635,26 @@ + + + - - - + + + - - - @@ -6654,26 +6662,26 @@ + + + - - - + + + - - - @@ -6681,18 +6689,18 @@ - - - - - - + + + + + + @@ -6719,32 +6727,32 @@ - - - - - - - - - - + + - - + + + + + + + + + + @@ -6757,29 +6765,29 @@ - - - - - - + + + + + + + + + - - - @@ -6790,15 +6798,15 @@ + + + - - - @@ -6806,54 +6814,54 @@ - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - @@ -6864,15 +6872,15 @@ + + + - - - @@ -6915,18 +6923,18 @@ - - - - - - + + + + + + @@ -6969,18 +6977,18 @@ - - - - - - + + + + + + @@ -6991,15 +6999,15 @@ + + + - - - @@ -7043,33 +7051,28 @@ + + + - - - - - - - - - - - - - + + + + + From 3517938b4b6ab923f9299201ee6c05cae8dcb954 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Sat, 28 Mar 2026 23:03:44 +0100 Subject: [PATCH 02/11] Add settings activity --- AndroidManifest.xml | 20 +++++++++++++ .../appsettings/redesign/SettingsActivity.kt | 25 ++++++++++++++++ .../appsettings/redesign/SettingsViewModel.kt | 30 +++++++++++++++++++ .../redesign/model/SettingsScreenEffect.kt | 5 ++++ .../redesign/model/SettingsUiState.kt | 8 +++++ .../redesign/screen/SettingsScreen.kt | 14 +++++++++ 6 files changed, 102 insertions(+) create mode 100644 src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/SettingsViewModel.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a1698f89..a55a1b2d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -204,6 +204,26 @@ + + + + + + + + + + val uiState: StateFlow +} + +@HiltViewModel +internal class SettingsViewModel @Inject constructor( +): ViewModel(), SettingsScreenModel { + + private val _effects = MutableSharedFlow(extraBufferCapacity = 1) + override val effects = _effects.asSharedFlow() + + private val _uiState = MutableStateFlow(SettingsUiState()) + override val uiState: StateFlow = _uiState.asStateFlow() + +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt new file mode 100644 index 00000000..71dfc9fd --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt @@ -0,0 +1,5 @@ +package com.android.messaging.ui.appsettings.redesign.model + +internal sealed interface SettingsScreenEffect { + // TODO: add effects +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt new file mode 100644 index 00000000..13526539 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt @@ -0,0 +1,8 @@ +package com.android.messaging.ui.appsettings.redesign.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class SettingsUiState( + // TODO: add properties +) diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt new file mode 100644 index 00000000..b984df58 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt @@ -0,0 +1,14 @@ +package com.android.messaging.ui.appsettings.redesign.screen + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.messaging.ui.appsettings.redesign.SettingsViewModel + +@Composable +internal fun SettingsScreen( + modifier: Modifier = Modifier, + viewModel: SettingsViewModel = viewModel(), +) { + +} From 83d1d107fbdbc6656369ef265fa2138bac0085ce Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Sun, 29 Mar 2026 20:50:27 +0200 Subject: [PATCH 03/11] Add settings main screen --- .../di/settings/SettingsBindsModule.kt | 27 +++++ .../appsettings/redesign/SettingsActivity.kt | 4 +- .../appsettings/redesign/SettingsViewModel.kt | 30 ----- .../redesign/common/SettingsComponents.kt | 105 ++++++++++++++++ .../redesign/common/SettingsScreenDelegate.kt | 11 ++ .../redesign/model/SettingsNavRoute.kt | 10 ++ .../redesign/model/SettingsUiState.kt | 4 +- .../redesign/screen/SettingsMainScreen.kt | 73 ++++++++++++ .../redesign/screen/SettingsScreen.kt | 69 ++++++++++- .../redesign/screen/SettingsViewModel.kt | 60 ++++++++++ .../delegate/SubscriptionSettingsDelegate.kt | 50 ++++++++ .../SubscriptionSettingsUiStateMapper.kt | 112 ++++++++++++++++++ .../model/SubscriptionSettingsUiState.kt | 10 ++ 13 files changed, 531 insertions(+), 34 deletions(-) create mode 100644 src/com/android/messaging/di/settings/SettingsBindsModule.kt delete mode 100644 src/com/android/messaging/ui/appsettings/redesign/SettingsViewModel.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/common/SettingsScreenDelegate.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/model/SettingsNavRoute.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt diff --git a/src/com/android/messaging/di/settings/SettingsBindsModule.kt b/src/com/android/messaging/di/settings/SettingsBindsModule.kt new file mode 100644 index 00000000..a6539470 --- /dev/null +++ b/src/com/android/messaging/di/settings/SettingsBindsModule.kt @@ -0,0 +1,27 @@ +package com.android.messaging.di.settings + +import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegate +import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegateImpl +import com.android.messaging.ui.appsettings.redesign.subscription.mapper.SubscriptionSettingsUiStateMapper +import com.android.messaging.ui.appsettings.redesign.subscription.mapper.SubscriptionSettingsUiStateMapperImpl +import dagger.Binds +import dagger.Module +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class SettingsBindsModule { + + @Binds + abstract fun bindSubscriptionSettingsDelegate( + impl: SubscriptionSettingsDelegateImpl, + ): SubscriptionSettingsDelegate + + @Binds + @Reusable + abstract fun bindSubscriptionSettingsUiStateMapper( + impl: SubscriptionSettingsUiStateMapperImpl, + ): SubscriptionSettingsUiStateMapper +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt b/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt index 7defdc5a..e373bba6 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt @@ -18,7 +18,9 @@ class SettingsActivity : ComponentActivity() { setContent { AppTheme { - SettingsScreen() + SettingsScreen( + onNavigateBack = ::finish, + ) } } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/SettingsViewModel.kt b/src/com/android/messaging/ui/appsettings/redesign/SettingsViewModel.kt deleted file mode 100644 index d3e76b64..00000000 --- a/src/com/android/messaging/ui/appsettings/redesign/SettingsViewModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.messaging.ui.appsettings.redesign - -import androidx.lifecycle.ViewModel -import com.android.messaging.ui.appsettings.redesign.model.SettingsScreenEffect -import com.android.messaging.ui.appsettings.redesign.model.SettingsUiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject - -internal interface SettingsScreenModel { - val effects: Flow - val uiState: StateFlow -} - -@HiltViewModel -internal class SettingsViewModel @Inject constructor( -): ViewModel(), SettingsScreenModel { - - private val _effects = MutableSharedFlow(extraBufferCapacity = 1) - override val effects = _effects.asSharedFlow() - - private val _uiState = MutableStateFlow(SettingsUiState()) - override val uiState: StateFlow = _uiState.asStateFlow() - -} diff --git a/src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt b/src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt new file mode 100644 index 00000000..499da7bd --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt @@ -0,0 +1,105 @@ +package com.android.messaging.ui.appsettings.redesign.common + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +internal fun SettingsClickableItem( + title: String, + modifier: Modifier = Modifier, + summary: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true, + onClick: (() -> Unit) = {}, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(enabled = enabled, onClick = onClick) + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (enabled) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, + ) + Spacer(modifier = Modifier.width(16.dp)) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (enabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, + ) + if (!summary.isNullOrEmpty()) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + color = if (enabled) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) + }, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SettingsClickableItemPreview() { + AppTheme { + Column { + SettingsClickableItem( + title = "Language", + summary = "English", + icon = Icons.Default.Language, + ) + SettingsClickableItem( + title = "Notifications", + icon = Icons.Default.Notifications, + ) + SettingsClickableItem( + title = "About", + summary = "Version 1.0.0", + ) + SettingsClickableItem( + title = "Disabled item", + summary = "Not available", + icon = Icons.Default.Lock, + enabled = false, + ) + } + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/common/SettingsScreenDelegate.kt b/src/com/android/messaging/ui/appsettings/redesign/common/SettingsScreenDelegate.kt new file mode 100644 index 00000000..2f8f6b96 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/common/SettingsScreenDelegate.kt @@ -0,0 +1,11 @@ +package com.android.messaging.ui.appsettings.redesign.common + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +internal interface SettingsScreenDelegate { + val state: StateFlow + + fun bind(scope: CoroutineScope) + fun refresh() +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsNavRoute.kt b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsNavRoute.kt new file mode 100644 index 00000000..59e84e67 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsNavRoute.kt @@ -0,0 +1,10 @@ +package com.android.messaging.ui.appsettings.redesign.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface SettingsNavRoute { + data object Main : SettingsNavRoute + data object AppSettings : SettingsNavRoute + data object SubscriptionSettings : SettingsNavRoute +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt index 13526539..c57d7570 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt @@ -1,8 +1,10 @@ package com.android.messaging.ui.appsettings.redesign.model import androidx.compose.runtime.Immutable +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState @Immutable internal data class SettingsUiState( - // TODO: add properties + val isMultiSim: Boolean = false, + val subscriptionSettings: List = emptyList(), ) diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt new file mode 100644 index 00000000..809784a1 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt @@ -0,0 +1,73 @@ +package com.android.messaging.ui.appsettings.redesign.screen + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import com.android.messaging.R +import com.android.messaging.ui.appsettings.redesign.common.SettingsClickableItem +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingsMainScreen( + subscriptions: List, + onNavigateBack: (() -> Unit), + onGeneralSettingsClick: (() -> Unit), + onSubscriptionClick: (() -> Unit), + modifier: Modifier = Modifier, +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { + Text(text = stringResource(R.string.settings_activity_title)) + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { contentPadding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding, + ) { + item { + SettingsClickableItem( + title = stringResource(R.string.general_settings), + onClick = onGeneralSettingsClick, + ) + } + + items(subscriptions) { subscription -> + SettingsClickableItem( + title = subscription.displayName, + summary = subscription.displayDetail, + onClick = onSubscriptionClick, + ) + } + } + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt index b984df58..c0e5dfe5 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt @@ -1,14 +1,79 @@ package com.android.messaging.ui.appsettings.redesign.screen +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.android.messaging.ui.appsettings.redesign.SettingsViewModel +import com.android.messaging.ui.appsettings.redesign.model.SettingsNavRoute @Composable internal fun SettingsScreen( + onNavigateBack: (() -> Unit), modifier: Modifier = Modifier, - viewModel: SettingsViewModel = viewModel(), + screenModel: SettingsScreenModel = viewModel(), ) { + val uiState by screenModel.uiState.collectAsStateWithLifecycle() + var currentRoute by remember { + mutableStateOf(SettingsNavRoute.Main) + } + + LifecycleEventEffect(event = Lifecycle.Event.ON_RESUME) { + screenModel.refreshState() + } + + // TODO: screen is blinking + // For single-SIM go directly to app settings + val effectiveRoute = if (!uiState.isMultiSim && currentRoute is SettingsNavRoute.Main) { + SettingsNavRoute.AppSettings + } else { + currentRoute + } + + AnimatedContent( + targetState = effectiveRoute, + modifier = modifier, + transitionSpec = { + val isForward = targetState != SettingsNavRoute.Main + if (isForward) { + (slideInHorizontally { it / 3 } + fadeIn()) togetherWith + (slideOutHorizontally { -it / 3 } + fadeOut()) + } else { + (slideInHorizontally { -it / 3 } + fadeIn()) togetherWith + (slideOutHorizontally { it / 3 } + fadeOut()) + } + }, + label = "settings_navigation", + ) { route -> + when (route) { + is SettingsNavRoute.Main -> { + SettingsMainScreen( + subscriptions = uiState.subscriptionSettings, + onNavigateBack = onNavigateBack, + onGeneralSettingsClick = { + currentRoute = SettingsNavRoute.AppSettings + }, + onSubscriptionClick = { + currentRoute = SettingsNavRoute.SubscriptionSettings + }, + ) + } + + is SettingsNavRoute.AppSettings -> {} + + is SettingsNavRoute.SubscriptionSettings -> {} + } + } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt new file mode 100644 index 00000000..f75ceaba --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt @@ -0,0 +1,60 @@ +package com.android.messaging.ui.appsettings.redesign.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android.messaging.ui.appsettings.redesign.model.SettingsScreenEffect +import com.android.messaging.ui.appsettings.redesign.model.SettingsUiState +import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegate +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +internal interface SettingsScreenModel { + val effects: Flow + val uiState: StateFlow + + fun refreshState() +} + +@HiltViewModel +internal class SettingsViewModel @Inject constructor( + private val subscriptionSettingsDelegate: SubscriptionSettingsDelegate, +) : ViewModel(), SettingsScreenModel { + + private val _effects = MutableSharedFlow(extraBufferCapacity = 1) + override val effects = _effects.asSharedFlow() + + override val uiState: StateFlow = subscriptionSettingsDelegate.state + .map { subscriptionState -> + SettingsUiState( + isMultiSim = subscriptionState.isMultiSim, + subscriptionSettings = subscriptionState.subscriptions, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(STATEFLOW_STOP_TIMEOUT_MILLIS), + initialValue = SettingsUiState(), + ) + + init { + initializeDelegates() + } + + private fun initializeDelegates() { + subscriptionSettingsDelegate.bind(scope = viewModelScope) + } + + override fun refreshState() { + subscriptionSettingsDelegate.refresh() + } + + private companion object { + private const val STATEFLOW_STOP_TIMEOUT_MILLIS = 5_000L + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt b/src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt new file mode 100644 index 00000000..899462ee --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt @@ -0,0 +1,50 @@ +package com.android.messaging.ui.appsettings.redesign.subscription.delegate + +import com.android.messaging.di.core.DefaultDispatcher +import com.android.messaging.ui.appsettings.redesign.common.SettingsScreenDelegate +import com.android.messaging.ui.appsettings.redesign.subscription.mapper.SubscriptionSettingsUiStateMapper +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal data class SubscriptionSettingsState( + val isMultiSim: Boolean = false, + val subscriptions: List = emptyList(), +) + +internal interface SubscriptionSettingsDelegate : + SettingsScreenDelegate + +internal class SubscriptionSettingsDelegateImpl @Inject constructor( + private val mapper: SubscriptionSettingsUiStateMapper, + @param:DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, +) : SubscriptionSettingsDelegate { + + private val _state = MutableStateFlow(SubscriptionSettingsState()) + override val state: StateFlow = _state.asStateFlow() + + private var boundScope: CoroutineScope? = null + private var isBound = false + + override fun bind(scope: CoroutineScope) { + if (isBound) return + isBound = true + boundScope = scope + refresh() + } + + override fun refresh() { + val scope = boundScope ?: return + scope.launch(defaultDispatcher) { + _state.value = SubscriptionSettingsState( + isMultiSim = mapper.isMultiSim(), + subscriptions = mapper.mapSubscriptions(), + ) + } + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt b/src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt new file mode 100644 index 00000000..61e94482 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt @@ -0,0 +1,112 @@ +package com.android.messaging.ui.appsettings.redesign.subscription.mapper + +import android.content.ContentResolver +import android.content.Context +import com.android.messaging.Factory +import com.android.messaging.R +import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns +import com.android.messaging.datamodel.MessagingContentProvider +import com.android.messaging.datamodel.data.ParticipantData +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState +import com.android.messaging.util.PhoneUtils +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal interface SubscriptionSettingsUiStateMapper { + fun isMultiSim(): Boolean + fun mapSubscriptions(): List +} + +internal class SubscriptionSettingsUiStateMapperImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val contentResolver: ContentResolver, +) : SubscriptionSettingsUiStateMapper { + + override fun isMultiSim(): Boolean { + return PhoneUtils.getDefault().activeSubscriptionCount > 1 + } + + override fun mapSubscriptions(): List { + if (!isMultiSim()) { + return listOf( + mapSingleSubscription( + subId = ParticipantData.DEFAULT_SELF_SUB_ID, + displayName = context.getString(R.string.advanced_settings), + ), + ) + } + + val selfParticipants = querySelfParticipants() + val nonDefaultSelfs = selfParticipants.filter { + !it.isDefaultSelf && it.isActiveSubscription + } + + return when { + nonDefaultSelfs.size > 1 -> nonDefaultSelfs.map { self -> + mapSingleSubscription( + subId = self.subId, + displayName = context.getString( + R.string.sim_specific_settings, + self.subscriptionName, + ), + ) + } + + nonDefaultSelfs.size == 1 -> listOf( + mapSingleSubscription( + subId = nonDefaultSelfs.first().subId, + displayName = context.getString(R.string.advanced_settings), + ), + ) + + else -> listOf( + mapSingleSubscription( + subId = ParticipantData.DEFAULT_SELF_SUB_ID, + displayName = context.getString(R.string.advanced_settings), + ), + ) + } + } + + private fun mapSingleSubscription( + subId: Int, + displayName: String, + ): SubscriptionSettingsUiState { + val subPrefs = Factory.get().getSubscriptionPrefs(subId) + val phoneUtils = PhoneUtils.get(subId) + + val phoneNumberKey = context.getString(R.string.mms_phone_number_pref_key) + val savedPhoneNumber = subPrefs.getString(phoneNumberKey, "") + val defaultPhoneNumber = phoneUtils.getCanonicalForSelf(false) + + val displayPhoneNumber = when { + !savedPhoneNumber.isNullOrEmpty() -> phoneUtils.formatForDisplay(savedPhoneNumber) + !defaultPhoneNumber.isNullOrEmpty() -> phoneUtils.formatForDisplay(defaultPhoneNumber) + else -> context.getString(R.string.unknown_phone_number_pref_display_value) + } + + return SubscriptionSettingsUiState( + subId = subId, + displayName = displayName, + displayDetail = displayPhoneNumber, + ) + } + + private fun querySelfParticipants(): List { + val cursor = contentResolver.query( + MessagingContentProvider.PARTICIPANTS_URI, + ParticipantData.ParticipantsQuery.PROJECTION, + ParticipantColumns.SUB_ID + " <> ?", + arrayOf(ParticipantData.OTHER_THAN_SELF_SUB_ID.toString()), + null, + ) ?: return emptyList() + + return cursor.use { + buildList { + while (it.moveToNext()) { + add(ParticipantData.getFromCursor(it)) + } + } + } + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt new file mode 100644 index 00000000..e3053eb2 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt @@ -0,0 +1,10 @@ +package com.android.messaging.ui.appsettings.redesign.subscription.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class SubscriptionSettingsUiState( + val subId: Int = -1, + val displayName: String = "", + val displayDetail: String = "", +) From f615bf2869f38b82f0b01f880dc8ff586f7ddec4 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Mon, 30 Mar 2026 00:35:10 +0200 Subject: [PATCH 04/11] Add subscription settings screen --- .../appsettings/redesign/SettingsActivity.kt | 26 ++ .../redesign/common/SettingsComponents.kt | 176 ++++++++- .../redesign/model/SettingsScreenEffect.kt | 5 - .../redesign/screen/SettingsEffectHandler.kt | 19 + .../redesign/screen/SettingsMainScreen.kt | 4 +- .../redesign/screen/SettingsScreen.kt | 59 ++- .../redesign/screen/SettingsViewModel.kt | 56 ++- .../{ => screen}/model/SettingsNavRoute.kt | 4 +- .../screen/model/SettingsScreenEffect.kt | 5 + .../{ => screen}/model/SettingsUiState.kt | 2 +- .../delegate/SubscriptionSettingsDelegate.kt | 59 ++- .../SubscriptionSettingsUiStateMapper.kt | 42 +++ .../model/SubscriptionSettingsUiState.kt | 10 + .../ui/SubscriptionSettingsScreen.kt | 335 ++++++++++++++++++ 14 files changed, 758 insertions(+), 44 deletions(-) delete mode 100644 src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt rename src/com/android/messaging/ui/appsettings/redesign/{ => screen}/model/SettingsNavRoute.kt (54%) create mode 100644 src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt rename src/com/android/messaging/ui/appsettings/redesign/{ => screen}/model/SettingsUiState.kt (82%) create mode 100644 src/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreen.kt diff --git a/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt b/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt index e373bba6..a2561a9a 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt @@ -4,6 +4,9 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import com.android.messaging.datamodel.data.ParticipantData +import com.android.messaging.ui.UIIntents +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsNavRoute import com.android.messaging.ui.appsettings.redesign.screen.SettingsScreen import com.android.messaging.ui.core.AppTheme import dagger.hilt.android.AndroidEntryPoint @@ -16,12 +19,35 @@ class SettingsActivity : ComponentActivity() { enableEdgeToEdge() + val initialRoute = resolveInitialRoute() + setContent { AppTheme { SettingsScreen( onNavigateBack = ::finish, + initialRoute = initialRoute, ) } } } + + private fun resolveInitialRoute(): SettingsNavRoute { + val subId = intent.getIntExtra( + /* name = */ UIIntents.UI_INTENT_EXTRA_SUB_ID, + /* defaultValue = */ ParticipantData.DEFAULT_SELF_SUB_ID, + ) + val subTitle = intent.getStringExtra( + /* name = */ UIIntents.UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE, + ) + val isTopLevel = intent.getBooleanExtra( + /* name = */ UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, + /* defaultValue = */ false, + ) + + return when { + subTitle != null -> SettingsNavRoute.SubscriptionSettings(subId, subTitle) + isTopLevel -> SettingsNavRoute.AppSettings + else -> SettingsNavRoute.Main + } + } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt b/src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt index 499da7bd..88fb03c9 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/common/SettingsComponents.kt @@ -8,45 +8,142 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.toggleable import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Block import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Mms import androidx.compose.material.icons.filled.Notifications import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.android.messaging.ui.core.AppTheme + +private const val DISABLED_ALPHA = 0.38f + +@Composable +private fun contentColor( + enabled: Boolean, + color: Color, +): Color { + return when { + enabled -> color + else -> color.copy(alpha = DISABLED_ALPHA) + } +} @Composable internal fun SettingsClickableItem( + title: String, + onClick: (() -> Unit), + modifier: Modifier = Modifier, + summary: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true, +) { + SettingsItemLayout( + title = title, + modifier = modifier.clickable( + enabled = enabled, + onClick = onClick, + ), + summary = summary, + icon = icon, + enabled = enabled, + verticalPadding = 16.dp, + ) +} + +@Composable +internal fun SettingsCategoryHeader( + title: String, + modifier: Modifier = Modifier, +) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + top = 24.dp, + bottom = 8.dp, + ), + ) +} + +@Composable +internal fun SettingsSwitchItem( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + summary: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true, +) { + SettingsItemLayout( + title = title, + modifier = modifier.toggleable( + value = checked, + enabled = enabled, + role = Role.Switch, + onValueChange = onCheckedChange, + ), + summary = summary, + icon = icon, + enabled = enabled, + verticalPadding = 12.dp, + trailing = { + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = checked, + onCheckedChange = null, + ) + }, + ) +} + +@Composable +private fun SettingsItemLayout( title: String, modifier: Modifier = Modifier, summary: String? = null, icon: ImageVector? = null, enabled: Boolean = true, - onClick: (() -> Unit) = {}, + verticalPadding: Dp = 16.dp, + trailing: @Composable (() -> Unit)? = null, ) { Row( modifier = modifier .fillMaxWidth() - .clickable(enabled = enabled, onClick = onClick) - .padding(horizontal = 16.dp, vertical = 16.dp), + .padding( + horizontal = 16.dp, + vertical = verticalPadding, + ), verticalAlignment = Alignment.CenterVertically, ) { if (icon != null) { Icon( imageVector = icon, contentDescription = null, - tint = if (enabled) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - }, + tint = contentColor( + enabled = enabled, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), ) Spacer(modifier = Modifier.width(16.dp)) } @@ -54,25 +151,24 @@ internal fun SettingsClickableItem( Text( text = title, style = MaterialTheme.typography.bodyLarge, - color = if (enabled) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - }, + color = contentColor( + enabled = enabled, + color = MaterialTheme.colorScheme.onSurface, + ), ) if (!summary.isNullOrEmpty()) { Spacer(modifier = Modifier.height(2.dp)) Text( text = summary, style = MaterialTheme.typography.bodyMedium, - color = if (enabled) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) - }, + color = contentColor( + enabled = enabled, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), ) } } + trailing?.invoke() } } @@ -85,20 +181,64 @@ private fun SettingsClickableItemPreview() { title = "Language", summary = "English", icon = Icons.Default.Language, + onClick = {}, ) SettingsClickableItem( title = "Notifications", icon = Icons.Default.Notifications, + onClick = {}, ) SettingsClickableItem( title = "About", summary = "Version 1.0.0", + onClick = {}, ) SettingsClickableItem( title = "Disabled item", summary = "Not available", icon = Icons.Default.Lock, enabled = false, + onClick = {}, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SettingsCategoryHeaderPreview() { + AppTheme { + SettingsCategoryHeader( + title = "MMS Messaging", + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SettingsSwitchItemPreview() { + AppTheme { + Column { + SettingsSwitchItem( + title = "Auto-retrieve MMS", + summary = "Automatically retrieve messages", + icon = Icons.Default.Mms, + checked = true, + onCheckedChange = {}, + ) + SettingsSwitchItem( + title = "Delivery reports", + summary = "Request a delivery report", + checked = false, + onCheckedChange = {}, + ) + SettingsSwitchItem( + title = "Disabled option", + summary = "Not available", + icon = Icons.Default.Block, + checked = false, + enabled = false, + onCheckedChange = {}, ) } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt b/src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt deleted file mode 100644 index 71dfc9fd..00000000 --- a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsScreenEffect.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.android.messaging.ui.appsettings.redesign.model - -internal sealed interface SettingsScreenEffect { - // TODO: add effects -} diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt new file mode 100644 index 00000000..48dd7d6f --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt @@ -0,0 +1,19 @@ +package com.android.messaging.ui.appsettings.redesign.screen + +import android.content.ActivityNotFoundException +import android.content.Context +import com.android.messaging.ui.UIIntents +import com.android.messaging.util.LogUtil +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsScreenEffect as Effect + +internal fun handleEffect(context: Context, effect: Effect) { + when (effect) { + is Effect.OpenWirelessAlerts -> { + try { + context.startActivity(UIIntents.get().wirelessAlertsIntent) + } catch (e: ActivityNotFoundException) { + LogUtil.e(LogUtil.BUGLE_TAG, "Failed to launch wireless alerts activity", e) + } + } + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt index 809784a1..b4b56caa 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsMainScreen.kt @@ -26,7 +26,7 @@ internal fun SettingsMainScreen( subscriptions: List, onNavigateBack: (() -> Unit), onGeneralSettingsClick: (() -> Unit), - onSubscriptionClick: (() -> Unit), + onSubscriptionClick: ((subId: Int, title: String) -> Unit), modifier: Modifier = Modifier, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -65,7 +65,7 @@ internal fun SettingsMainScreen( SettingsClickableItem( title = subscription.displayName, summary = subscription.displayDetail, - onClick = onSubscriptionClick, + onClick = { onSubscriptionClick(subscription.subId, subscription.displayName) }, ) } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt index c0e5dfe5..9115c307 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt @@ -1,5 +1,6 @@ package com.android.messaging.ui.appsettings.redesign.screen +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -7,34 +8,44 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.android.messaging.ui.appsettings.redesign.model.SettingsNavRoute +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsNavRoute +import com.android.messaging.ui.appsettings.redesign.subscription.ui.SubscriptionSettingsScreen @Composable internal fun SettingsScreen( onNavigateBack: (() -> Unit), modifier: Modifier = Modifier, + initialRoute: SettingsNavRoute = SettingsNavRoute.Main, screenModel: SettingsScreenModel = viewModel(), ) { + val context = LocalContext.current val uiState by screenModel.uiState.collectAsStateWithLifecycle() var currentRoute by remember { - mutableStateOf(SettingsNavRoute.Main) + mutableStateOf(initialRoute) } LifecycleEventEffect(event = Lifecycle.Event.ON_RESUME) { screenModel.refreshState() } - // TODO: screen is blinking + LaunchedEffect(screenModel, context) { + screenModel.effects.collect { effect -> + handleEffect(context, effect) + } + } + // For single-SIM go directly to app settings val effectiveRoute = if (!uiState.isMultiSim && currentRoute is SettingsNavRoute.Main) { SettingsNavRoute.AppSettings @@ -42,6 +53,32 @@ internal fun SettingsScreen( currentRoute } + val isRootRoute = effectiveRoute is SettingsNavRoute.Main || + (effectiveRoute is SettingsNavRoute.AppSettings && !uiState.isMultiSim) + + val navigateUp: (() -> Unit) = { + when { + isRootRoute -> onNavigateBack() + + effectiveRoute is SettingsNavRoute.AppSettings -> { + currentRoute = SettingsNavRoute.Main + } + + effectiveRoute is SettingsNavRoute.SubscriptionSettings -> { + currentRoute = if (uiState.isMultiSim) { + SettingsNavRoute.Main + } else { + SettingsNavRoute.AppSettings + } + } + } + } + + BackHandler( + enabled = !isRootRoute, + onBack = navigateUp, + ) + AnimatedContent( targetState = effectiveRoute, modifier = modifier, @@ -65,15 +102,25 @@ internal fun SettingsScreen( onGeneralSettingsClick = { currentRoute = SettingsNavRoute.AppSettings }, - onSubscriptionClick = { - currentRoute = SettingsNavRoute.SubscriptionSettings + onSubscriptionClick = { subId, title -> + currentRoute = SettingsNavRoute.SubscriptionSettings(subId, title) }, ) } is SettingsNavRoute.AppSettings -> {} - is SettingsNavRoute.SubscriptionSettings -> {} + is SettingsNavRoute.SubscriptionSettings -> { + val sub = uiState.subscriptionSettings.find { it.subId == route.subId } + if (sub != null) { + SubscriptionSettingsScreen( + subscriptionSettings = sub, + title = route.title, + screenModel = screenModel, + onNavigateBack = navigateUp, + ) + } + } } } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt index f75ceaba..c619317a 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt @@ -2,8 +2,6 @@ package com.android.messaging.ui.appsettings.redesign.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.android.messaging.ui.appsettings.redesign.model.SettingsScreenEffect -import com.android.messaging.ui.appsettings.redesign.model.SettingsUiState import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegate import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow @@ -13,13 +11,23 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsScreenEffect as Effect +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsUiState as State internal interface SettingsScreenModel { - val effects: Flow - val uiState: StateFlow + val effects: Flow + val uiState: StateFlow fun refreshState() + + fun onAutoRetrieveMmsChanged(subId: Int, enabled: Boolean) + fun onAutoRetrieveMmsWhenRoamingChanged(subId: Int, enabled: Boolean) + fun onDeliveryReportsChanged(subId: Int, enabled: Boolean) + fun onWirelessAlertsClick(subId: Int) + fun onGroupMmsChanged(subId: Int, enabled: Boolean) + fun onPhoneNumberChanged(subId: Int, phoneNumber: String) } @HiltViewModel @@ -27,19 +35,19 @@ internal class SettingsViewModel @Inject constructor( private val subscriptionSettingsDelegate: SubscriptionSettingsDelegate, ) : ViewModel(), SettingsScreenModel { - private val _effects = MutableSharedFlow(extraBufferCapacity = 1) - override val effects = _effects.asSharedFlow() + private val _effects = MutableSharedFlow(extraBufferCapacity = 1) + override val effects: Flow = _effects.asSharedFlow() - override val uiState: StateFlow = subscriptionSettingsDelegate.state + override val uiState: StateFlow = subscriptionSettingsDelegate.state .map { subscriptionState -> - SettingsUiState( + State( isMultiSim = subscriptionState.isMultiSim, subscriptionSettings = subscriptionState.subscriptions, ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(STATEFLOW_STOP_TIMEOUT_MILLIS), - initialValue = SettingsUiState(), + initialValue = State(), ) init { @@ -54,6 +62,36 @@ internal class SettingsViewModel @Inject constructor( subscriptionSettingsDelegate.refresh() } + override fun onAutoRetrieveMmsChanged(subId: Int, enabled: Boolean) { + subscriptionSettingsDelegate.onAutoRetrieveMmsChanged(subId, enabled) + } + + override fun onAutoRetrieveMmsWhenRoamingChanged(subId: Int, enabled: Boolean) { + subscriptionSettingsDelegate.onAutoRetrieveMmsWhenRoamingChanged(subId, enabled) + } + + override fun onDeliveryReportsChanged(subId: Int, enabled: Boolean) { + subscriptionSettingsDelegate.onDeliveryReportsChanged(subId, enabled) + } + + override fun onGroupMmsChanged(subId: Int, enabled: Boolean) { + subscriptionSettingsDelegate.onGroupMmsChanged(subId, enabled) + } + + override fun onPhoneNumberChanged(subId: Int, phoneNumber: String) { + subscriptionSettingsDelegate.onPhoneNumberChanged(subId, phoneNumber) + } + + override fun onWirelessAlertsClick(subId: Int) { + emitEffect(Effect.OpenWirelessAlerts(subId)) + } + + private fun emitEffect(effect: Effect) { + viewModelScope.launch { + _effects.emit(effect) + } + } + private companion object { private const val STATEFLOW_STOP_TIMEOUT_MILLIS = 5_000L } diff --git a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsNavRoute.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsNavRoute.kt similarity index 54% rename from src/com/android/messaging/ui/appsettings/redesign/model/SettingsNavRoute.kt rename to src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsNavRoute.kt index 59e84e67..2af10fed 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsNavRoute.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsNavRoute.kt @@ -1,4 +1,4 @@ -package com.android.messaging.ui.appsettings.redesign.model +package com.android.messaging.ui.appsettings.redesign.screen.model import androidx.compose.runtime.Immutable @@ -6,5 +6,5 @@ import androidx.compose.runtime.Immutable internal sealed interface SettingsNavRoute { data object Main : SettingsNavRoute data object AppSettings : SettingsNavRoute - data object SubscriptionSettings : SettingsNavRoute + data class SubscriptionSettings(val subId: Int, val title: String) : SettingsNavRoute } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt new file mode 100644 index 00000000..573b7bcb --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt @@ -0,0 +1,5 @@ +package com.android.messaging.ui.appsettings.redesign.screen.model + +internal sealed interface SettingsScreenEffect { + data class OpenWirelessAlerts(val subId: Int) : SettingsScreenEffect +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt similarity index 82% rename from src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt rename to src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt index c57d7570..4c56ec1e 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/model/SettingsUiState.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt @@ -1,4 +1,4 @@ -package com.android.messaging.ui.appsettings.redesign.model +package com.android.messaging.ui.appsettings.redesign.screen.model import androidx.compose.runtime.Immutable import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState diff --git a/src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt b/src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt index 899462ee..3458820b 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/subscription/delegate/SubscriptionSettingsDelegate.kt @@ -1,9 +1,15 @@ package com.android.messaging.ui.appsettings.redesign.subscription.delegate +import android.content.Context +import com.android.messaging.R +import com.android.messaging.datamodel.ParticipantRefresh import com.android.messaging.di.core.DefaultDispatcher import com.android.messaging.ui.appsettings.redesign.common.SettingsScreenDelegate import com.android.messaging.ui.appsettings.redesign.subscription.mapper.SubscriptionSettingsUiStateMapper import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState +import com.android.messaging.util.BuglePrefs +import com.android.messaging.util.PhoneUtils +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -18,9 +24,16 @@ internal data class SubscriptionSettingsState( ) internal interface SubscriptionSettingsDelegate : - SettingsScreenDelegate + SettingsScreenDelegate { + fun onAutoRetrieveMmsChanged(subId: Int, enabled: Boolean) + fun onAutoRetrieveMmsWhenRoamingChanged(subId: Int, enabled: Boolean) + fun onDeliveryReportsChanged(subId: Int, enabled: Boolean) + fun onGroupMmsChanged(subId: Int, enabled: Boolean) + fun onPhoneNumberChanged(subId: Int, phoneNumber: String) +} internal class SubscriptionSettingsDelegateImpl @Inject constructor( + @ApplicationContext private val context: Context, private val mapper: SubscriptionSettingsUiStateMapper, @param:DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, ) : SubscriptionSettingsDelegate { @@ -47,4 +60,48 @@ internal class SubscriptionSettingsDelegateImpl @Inject constructor( ) } } + + override fun onAutoRetrieveMmsChanged(subId: Int, enabled: Boolean) { + val key = context.getString(R.string.auto_retrieve_mms_pref_key) + BuglePrefs.getSubscriptionPrefs(subId).putBoolean(key, enabled) + refresh() + } + + override fun onAutoRetrieveMmsWhenRoamingChanged(subId: Int, enabled: Boolean) { + val key = context.getString(R.string.auto_retrieve_mms_when_roaming_pref_key) + BuglePrefs.getSubscriptionPrefs(subId).putBoolean(key, enabled) + refresh() + } + + override fun onDeliveryReportsChanged(subId: Int, enabled: Boolean) { + val key = context.getString(R.string.delivery_reports_pref_key) + BuglePrefs.getSubscriptionPrefs(subId).putBoolean(key, enabled) + refresh() + } + + override fun onGroupMmsChanged(subId: Int, enabled: Boolean) { + val key = context.getString(R.string.group_mms_pref_key) + BuglePrefs.getSubscriptionPrefs(subId).putBoolean(key, enabled) + refresh() + } + + override fun onPhoneNumberChanged(subId: Int, phoneNumber: String) { + val phoneUtils = PhoneUtils.get(subId) + + val canonical = phoneUtils.getCanonicalBySystemLocale(phoneNumber) + val defaultCanonical = phoneUtils.getCanonicalBySystemLocale( + phoneUtils.getCanonicalForSelf(false), + ) + + val key = context.getString(R.string.mms_phone_number_pref_key) + val subPrefs = BuglePrefs.getSubscriptionPrefs(subId) + if (canonical == defaultCanonical || phoneNumber.isEmpty()) { + subPrefs.remove(key) + } else { + subPrefs.putString(key, phoneNumber) + } + + ParticipantRefresh.refreshSelfParticipants() + refresh() + } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt b/src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt index 61e94482..d727dff3 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/subscription/mapper/SubscriptionSettingsUiStateMapper.kt @@ -2,11 +2,14 @@ package com.android.messaging.ui.appsettings.redesign.subscription.mapper import android.content.ContentResolver import android.content.Context +import android.content.pm.PackageManager import com.android.messaging.Factory import com.android.messaging.R import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns import com.android.messaging.datamodel.MessagingContentProvider import com.android.messaging.datamodel.data.ParticipantData +import com.android.messaging.sms.MmsConfig +import com.android.messaging.ui.UIIntents import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState import com.android.messaging.util.PhoneUtils import dagger.hilt.android.qualifiers.ApplicationContext @@ -74,6 +77,7 @@ internal class SubscriptionSettingsUiStateMapperImpl @Inject constructor( ): SubscriptionSettingsUiState { val subPrefs = Factory.get().getSubscriptionPrefs(subId) val phoneUtils = PhoneUtils.get(subId) + val mmsConfig = MmsConfig.get(subId) val phoneNumberKey = context.getString(R.string.mms_phone_number_pref_key) val savedPhoneNumber = subPrefs.getString(phoneNumberKey, "") @@ -85,10 +89,39 @@ internal class SubscriptionSettingsUiStateMapperImpl @Inject constructor( else -> context.getString(R.string.unknown_phone_number_pref_display_value) } + val isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp + val groupMmsPrefKey = context.getString(R.string.group_mms_pref_key) + val autoRetrieveKey = context.getString(R.string.auto_retrieve_mms_pref_key) + val autoRetrieveRoamingKey = + context.getString(R.string.auto_retrieve_mms_when_roaming_pref_key) + val deliveryReportsKey = context.getString(R.string.delivery_reports_pref_key) + return SubscriptionSettingsUiState( subId = subId, displayName = displayName, displayDetail = displayPhoneNumber, + phoneNumber = savedPhoneNumber.orEmpty(), + defaultPhoneNumber = defaultPhoneNumber.orEmpty(), + isGroupMmsSupported = mmsConfig.groupMmsEnabled, + isGroupMmsEnabled = subPrefs.getBoolean( + groupMmsPrefKey, + context.resources.getBoolean(R.bool.group_mms_pref_default), + ), + autoRetrieveMms = subPrefs.getBoolean( + autoRetrieveKey, + context.resources.getBoolean(R.bool.auto_retrieve_mms_pref_default), + ), + autoRetrieveMmsWhenRoaming = subPrefs.getBoolean( + autoRetrieveRoamingKey, + context.resources.getBoolean(R.bool.auto_retrieve_mms_when_roaming_pref_default), + ), + isDeliveryReportsSupported = mmsConfig.smsDeliveryReportsEnabled, + deliveryReportsEnabled = subPrefs.getBoolean( + deliveryReportsKey, + context.resources.getBoolean(R.bool.delivery_reports_pref_default), + ), + isWirelessAlertsSupported = mmsConfig.showCellBroadcast && isCellBroadcastAppEnabled(), + isDefaultSmsApp = isDefaultSmsApp, ) } @@ -109,4 +142,13 @@ internal class SubscriptionSettingsUiStateMapperImpl @Inject constructor( } } } + + private fun isCellBroadcastAppEnabled(): Boolean { + return try { + context.packageManager + .getApplicationEnabledSetting(UIIntents.CMAS_COMPONENT) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } catch (_: IllegalArgumentException) { + false + } + } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt index e3053eb2..f42797a5 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/subscription/model/SubscriptionSettingsUiState.kt @@ -7,4 +7,14 @@ internal data class SubscriptionSettingsUiState( val subId: Int = -1, val displayName: String = "", val displayDetail: String = "", + val phoneNumber: String = "", + val defaultPhoneNumber: String = "", + val isGroupMmsSupported: Boolean = false, + val isGroupMmsEnabled: Boolean = true, + val autoRetrieveMms: Boolean = true, + val autoRetrieveMmsWhenRoaming: Boolean = false, + val isDeliveryReportsSupported: Boolean = false, + val deliveryReportsEnabled: Boolean = false, + val isWirelessAlertsSupported: Boolean = false, + val isDefaultSmsApp: Boolean = false, ) diff --git a/src/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreen.kt new file mode 100644 index 00000000..0a5af14c --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreen.kt @@ -0,0 +1,335 @@ +package com.android.messaging.ui.appsettings.redesign.subscription.ui + +import androidx.compose.foundation.layout.Column +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.messaging.R +import com.android.messaging.ui.appsettings.redesign.common.SettingsCategoryHeader +import com.android.messaging.ui.appsettings.redesign.common.SettingsClickableItem +import com.android.messaging.ui.appsettings.redesign.common.SettingsSwitchItem +import com.android.messaging.ui.appsettings.redesign.screen.SettingsScreenModel +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState +import com.android.messaging.ui.core.AppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SubscriptionSettingsScreen( + subscriptionSettings: SubscriptionSettingsUiState, + title: String, + screenModel: SettingsScreenModel, + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + var showGroupMmsDialog by remember { mutableStateOf(false) } + var showPhoneNumberDialog by remember { mutableStateOf(false) } + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text(title) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { contentPadding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding, + ) { + item(key = "mms_category_header") { + SettingsCategoryHeader( + title = stringResource(R.string.mms_messaging_category_pref_title), + ) + } + + if (subscriptionSettings.isGroupMmsSupported) { + item(key = "group_mms") { + SettingsClickableItem( + title = stringResource(R.string.group_mms_pref_title), + summary = if (subscriptionSettings.isGroupMmsEnabled) { + stringResource(R.string.enable_group_mms) + } else { + stringResource(R.string.disable_group_mms) + }, + enabled = subscriptionSettings.isDefaultSmsApp, + onClick = { showGroupMmsDialog = true }, + ) + } + } + + item(key = "phone_number") { + SettingsClickableItem( + title = stringResource(R.string.mms_phone_number_pref_title), + summary = subscriptionSettings.displayDetail, + onClick = { showPhoneNumberDialog = true }, + ) + } + + item(key = "auto_retrieve_mms") { + SettingsSwitchItem( + title = stringResource(R.string.auto_retrieve_mms_pref_title), + summary = stringResource(R.string.auto_retrieve_mms_pref_summary), + checked = subscriptionSettings.autoRetrieveMms, + enabled = subscriptionSettings.isDefaultSmsApp, + onCheckedChange = { enabled -> + screenModel.onAutoRetrieveMmsChanged(subscriptionSettings.subId, enabled) + }, + ) + } + + item(key = "auto_retrieve_mms_roaming") { + SettingsSwitchItem( + title = stringResource(R.string.auto_retrieve_mms_when_roaming_pref_title), + summary = stringResource(R.string.auto_retrieve_mms_when_roaming_pref_summary), + checked = subscriptionSettings.autoRetrieveMmsWhenRoaming, + enabled = subscriptionSettings.isDefaultSmsApp && subscriptionSettings.autoRetrieveMms, + onCheckedChange = { enabled -> + screenModel.onAutoRetrieveMmsWhenRoamingChanged( + subscriptionSettings.subId, + enabled, + ) + }, + ) + } + + val hasAdvanced = subscriptionSettings.isDeliveryReportsSupported || + subscriptionSettings.isWirelessAlertsSupported + if (hasAdvanced) { + item(key = "advanced_divider") { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + } + item(key = "advanced_category_header") { + SettingsCategoryHeader( + title = stringResource(R.string.advanced_category_pref_title), + ) + } + } + + if (subscriptionSettings.isDeliveryReportsSupported) { + item(key = "delivery_reports") { + SettingsSwitchItem( + title = stringResource(R.string.delivery_reports_pref_title), + summary = stringResource(R.string.delivery_reports_pref_summary), + checked = subscriptionSettings.deliveryReportsEnabled, + enabled = subscriptionSettings.isDefaultSmsApp, + onCheckedChange = { enabled -> + screenModel.onDeliveryReportsChanged( + subscriptionSettings.subId, + enabled, + ) + }, + ) + } + } + + if (subscriptionSettings.isWirelessAlertsSupported) { + item(key = "wireless_alerts") { + SettingsClickableItem( + title = stringResource(R.string.wireless_alerts_title), + onClick = { screenModel.onWirelessAlertsClick(subscriptionSettings.subId) }, + ) + } + } + } + } + + if (showGroupMmsDialog) { + GroupMmsDialog( + isEnabled = subscriptionSettings.isGroupMmsEnabled, + onDismiss = { showGroupMmsDialog = false }, + onConfirm = { enabled -> + screenModel.onGroupMmsChanged(subscriptionSettings.subId, enabled) + showGroupMmsDialog = false + }, + ) + } + + if (showPhoneNumberDialog) { + PhoneNumberDialog( + currentNumber = subscriptionSettings.phoneNumber.ifEmpty { + subscriptionSettings.defaultPhoneNumber + }, + onDismiss = { showPhoneNumberDialog = false }, + onConfirm = { phoneNumber -> + screenModel.onPhoneNumberChanged(subscriptionSettings.subId, phoneNumber) + showPhoneNumberDialog = false + }, + ) + } +} + +@Composable +private fun GroupMmsDialog( + isEnabled: Boolean, + onDismiss: () -> Unit, + onConfirm: (Boolean) -> Unit, +) { + var selectedEnabled by remember { mutableStateOf(isEnabled) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = stringResource(R.string.group_mms_pref_title)) + }, + text = { + Column(modifier = Modifier.selectableGroup()) { + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selectedEnabled, + onClick = { selectedEnabled = true }, + role = Role.RadioButton, + ) + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = selectedEnabled, + onClick = null, + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(R.string.enable_group_mms), + style = MaterialTheme.typography.bodyLarge, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = !selectedEnabled, + onClick = { selectedEnabled = false }, + role = Role.RadioButton, + ) + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = !selectedEnabled, + onClick = null, + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(R.string.disable_group_mms), + style = MaterialTheme.typography.bodyLarge, + ) + } + } + }, + confirmButton = { + TextButton(onClick = { onConfirm(selectedEnabled) }) { + Text(text = stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + ) +} + +@Preview +@Composable +private fun GroupMmsDialogPreview() { + AppTheme { + GroupMmsDialog( + isEnabled = true, + onDismiss = {}, + onConfirm = {}, + ) + } +} + +@Composable +private fun PhoneNumberDialog( + currentNumber: String, + onDismiss: () -> Unit, + onConfirm: (String) -> Unit, +) { + var phoneNumber by remember { mutableStateOf(currentNumber) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = stringResource(R.string.mms_phone_number_pref_title)) + }, + text = { + OutlinedTextField( + value = phoneNumber, + onValueChange = { phoneNumber = it }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + singleLine = true, + ) + }, + confirmButton = { + TextButton(onClick = { onConfirm(phoneNumber) }) { + Text(text = stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + ) +} + +@Preview +@Composable +private fun PhoneNumberDialogPreview() { + AppTheme { + PhoneNumberDialog( + currentNumber = "+31 6 1234 5678", + onDismiss = {}, + onConfirm = {}, + ) + } +} From 2e0cd0d81988f2343fa4975efe13e768c6acc99d Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Wed, 1 Apr 2026 21:07:05 +0200 Subject: [PATCH 05/11] Add app settings screen --- .../di/settings/SettingsBindsModule.kt | 15 ++ .../delegate/AppSettingsDelegate.kt | 68 +++++++++ .../mapper/AppSettingsUiStateMapper.kt | 49 +++++++ .../appsettings/model/AppSettingsUiState.kt | 13 ++ .../appsettings/ui/AppSettingsScreen.kt | 135 ++++++++++++++++++ .../redesign/screen/SettingsEffectHandler.kt | 52 ++++++- .../redesign/screen/SettingsScreen.kt | 28 +++- .../redesign/screen/SettingsViewModel.kt | 68 +++++++-- .../screen/model/SettingsScreenEffect.kt | 4 + .../redesign/screen/model/SettingsUiState.kt | 2 + 10 files changed, 410 insertions(+), 24 deletions(-) create mode 100644 src/com/android/messaging/ui/appsettings/redesign/appsettings/delegate/AppSettingsDelegate.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/appsettings/mapper/AppSettingsUiStateMapper.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/appsettings/model/AppSettingsUiState.kt create mode 100644 src/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreen.kt diff --git a/src/com/android/messaging/di/settings/SettingsBindsModule.kt b/src/com/android/messaging/di/settings/SettingsBindsModule.kt index a6539470..7f908aac 100644 --- a/src/com/android/messaging/di/settings/SettingsBindsModule.kt +++ b/src/com/android/messaging/di/settings/SettingsBindsModule.kt @@ -1,5 +1,9 @@ package com.android.messaging.di.settings +import com.android.messaging.ui.appsettings.redesign.appsettings.delegate.AppSettingsDelegate +import com.android.messaging.ui.appsettings.redesign.appsettings.delegate.AppSettingsDelegateImpl +import com.android.messaging.ui.appsettings.redesign.appsettings.mapper.AppSettingsUiStateMapper +import com.android.messaging.ui.appsettings.redesign.appsettings.mapper.AppSettingsUiStateMapperImpl import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegate import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegateImpl import com.android.messaging.ui.appsettings.redesign.subscription.mapper.SubscriptionSettingsUiStateMapper @@ -24,4 +28,15 @@ internal abstract class SettingsBindsModule { abstract fun bindSubscriptionSettingsUiStateMapper( impl: SubscriptionSettingsUiStateMapperImpl, ): SubscriptionSettingsUiStateMapper + + @Binds + abstract fun bindAppSettingsDelegate( + impl: AppSettingsDelegateImpl, + ): AppSettingsDelegate + + @Binds + @Reusable + abstract fun bindAppSettingsUiStateMapper( + impl: AppSettingsUiStateMapperImpl, + ): AppSettingsUiStateMapper } diff --git a/src/com/android/messaging/ui/appsettings/redesign/appsettings/delegate/AppSettingsDelegate.kt b/src/com/android/messaging/ui/appsettings/redesign/appsettings/delegate/AppSettingsDelegate.kt new file mode 100644 index 00000000..71add63e --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/appsettings/delegate/AppSettingsDelegate.kt @@ -0,0 +1,68 @@ +package com.android.messaging.ui.appsettings.redesign.appsettings.delegate + +import android.content.Context +import com.android.messaging.R +import com.android.messaging.di.core.DefaultDispatcher +import com.android.messaging.ui.appsettings.redesign.appsettings.mapper.AppSettingsUiStateMapper +import com.android.messaging.ui.appsettings.redesign.appsettings.model.AppSettingsUiState +import com.android.messaging.ui.appsettings.redesign.common.SettingsScreenDelegate +import com.android.messaging.util.BuglePrefs +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal interface AppSettingsDelegate : SettingsScreenDelegate { + fun onSendSoundChanged(enabled: Boolean) + fun onDumpSmsChanged(enabled: Boolean) + fun onDumpMmsChanged(enabled: Boolean) +} + +internal class AppSettingsDelegateImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val mapper: AppSettingsUiStateMapper, + @param:DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, +) : AppSettingsDelegate { + + private val _state = MutableStateFlow(AppSettingsUiState()) + override val state: StateFlow = _state.asStateFlow() + + private var boundScope: CoroutineScope? = null + private var isBound = false + + override fun bind(scope: CoroutineScope) { + if (isBound) return + isBound = true + boundScope = scope + refresh() + } + + override fun refresh() { + val scope = boundScope ?: return + scope.launch(defaultDispatcher) { + _state.value = mapper.map() + } + } + + override fun onSendSoundChanged(enabled: Boolean) { + val key = context.getString(R.string.send_sound_pref_key) + BuglePrefs.getApplicationPrefs().putBoolean(key, enabled) + refresh() + } + + override fun onDumpSmsChanged(enabled: Boolean) { + val key = context.getString(R.string.dump_sms_pref_key) + BuglePrefs.getApplicationPrefs().putBoolean(key, enabled) + refresh() + } + + override fun onDumpMmsChanged(enabled: Boolean) { + val key = context.getString(R.string.dump_mms_pref_key) + BuglePrefs.getApplicationPrefs().putBoolean(key, enabled) + refresh() + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/appsettings/mapper/AppSettingsUiStateMapper.kt b/src/com/android/messaging/ui/appsettings/redesign/appsettings/mapper/AppSettingsUiStateMapper.kt new file mode 100644 index 00000000..e12b0df2 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/appsettings/mapper/AppSettingsUiStateMapper.kt @@ -0,0 +1,49 @@ +package com.android.messaging.ui.appsettings.redesign.appsettings.mapper + +import android.content.Context +import com.android.messaging.R +import com.android.messaging.ui.appsettings.redesign.appsettings.model.AppSettingsUiState +import com.android.messaging.util.BuglePrefs +import com.android.messaging.util.DebugUtils +import com.android.messaging.util.PhoneUtils +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal interface AppSettingsUiStateMapper { + fun map(): AppSettingsUiState +} + +internal class AppSettingsUiStateMapperImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : AppSettingsUiStateMapper { + + override fun map(): AppSettingsUiState { + val appPrefs = BuglePrefs.getApplicationPrefs() + val phoneUtils = PhoneUtils.getDefault() + + val sendSoundKey = context.getString(R.string.send_sound_pref_key) + val dumpSmsKey = context.getString(R.string.dump_sms_pref_key) + val dumpMmsKey = context.getString(R.string.dump_mms_pref_key) + + return AppSettingsUiState( + isDefaultSmsApp = phoneUtils.isDefaultSmsApp, + defaultSmsAppLabel = context.getString( + R.string.default_sms_app, + phoneUtils.defaultSmsAppLabel, + ), + sendSoundEnabled = appPrefs.getBoolean( + sendSoundKey, + context.resources.getBoolean(R.bool.send_sound_pref_default), + ), + isDebugEnabled = DebugUtils.isDebugEnabled(), + dumpSmsEnabled = appPrefs.getBoolean( + dumpSmsKey, + context.resources.getBoolean(R.bool.dump_sms_pref_default), + ), + dumpMmsEnabled = appPrefs.getBoolean( + dumpMmsKey, + context.resources.getBoolean(R.bool.dump_mms_pref_default), + ), + ) + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/appsettings/model/AppSettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/appsettings/model/AppSettingsUiState.kt new file mode 100644 index 00000000..d7b4f541 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/appsettings/model/AppSettingsUiState.kt @@ -0,0 +1,13 @@ +package com.android.messaging.ui.appsettings.redesign.appsettings.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class AppSettingsUiState( + val isDefaultSmsApp: Boolean = false, + val defaultSmsAppLabel: String = "", + val sendSoundEnabled: Boolean = true, + val isDebugEnabled: Boolean = false, + val dumpSmsEnabled: Boolean = false, + val dumpMmsEnabled: Boolean = false, +) diff --git a/src/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreen.kt new file mode 100644 index 00000000..12abb590 --- /dev/null +++ b/src/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreen.kt @@ -0,0 +1,135 @@ +package com.android.messaging.ui.appsettings.redesign.appsettings.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import com.android.messaging.R +import com.android.messaging.ui.appsettings.redesign.appsettings.model.AppSettingsUiState +import com.android.messaging.ui.appsettings.redesign.common.SettingsCategoryHeader +import com.android.messaging.ui.appsettings.redesign.common.SettingsClickableItem +import com.android.messaging.ui.appsettings.redesign.common.SettingsSwitchItem +import com.android.messaging.ui.appsettings.redesign.screen.SettingsScreenModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AppSettingsScreen( + appSettings: AppSettingsUiState, + screenModel: SettingsScreenModel, + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, + isTopLevel: Boolean = false, + onAdvancedClick: (() -> Unit)? = null, +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val title = if (isTopLevel) { + stringResource(R.string.settings_activity_title) + } else { + stringResource(R.string.general_settings_activity_title) + } + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text(text = title) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { contentPadding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding, + ) { + item(key = "default_sms_app") { + SettingsClickableItem( + title = stringResource(R.string.sms_disabled_pref_title), + summary = appSettings.defaultSmsAppLabel, + onClick = { screenModel.onDefaultSmsAppClick(appSettings.isDefaultSmsApp) }, + ) + } + + item(key = "notifications") { + SettingsClickableItem( + title = stringResource(R.string.notifications_enabled_conversation_pref_title), + onClick = { screenModel.onNotificationsClick() }, + ) + } + + item(key = "send_sound") { + SettingsSwitchItem( + title = stringResource(R.string.send_sound_pref_title), + checked = appSettings.sendSoundEnabled, + onCheckedChange = screenModel::onSendSoundChanged, + ) + } + + if (isTopLevel && onAdvancedClick != null) { + item(key = "advanced_settings") { + SettingsClickableItem( + title = stringResource(R.string.advanced_settings), + onClick = onAdvancedClick, + ) + } + } + + if (appSettings.isDebugEnabled) { + item(key = "debug_divider") { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + } + item(key = "debug_category_header") { + SettingsCategoryHeader( + title = stringResource(R.string.debug_category_pref_title), + ) + } + item(key = "dump_sms") { + SettingsSwitchItem( + title = stringResource(R.string.dump_sms_pref_title), + summary = stringResource(R.string.dump_sms_pref_summary), + checked = appSettings.dumpSmsEnabled, + onCheckedChange = screenModel::onDumpSmsChanged, + ) + } + item(key = "dump_mms") { + SettingsSwitchItem( + title = stringResource(R.string.dump_mms_pref_title), + summary = stringResource(R.string.dump_mms_pref_summary), + checked = appSettings.dumpMmsEnabled, + onCheckedChange = screenModel::onDumpMmsChanged, + ) + } + } + + item(key = "licenses_divider") { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + } + item(key = "licenses") { + SettingsClickableItem( + title = stringResource(R.string.menu_license), + onClick = { screenModel.onLicensesClick() }, + ) + } + } + } +} diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt index 48dd7d6f..cf3b0b5a 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsEffectHandler.kt @@ -1,18 +1,56 @@ package com.android.messaging.ui.appsettings.redesign.screen +import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.Context +import android.content.Intent +import android.provider.Settings +import com.android.messaging.ui.LicenseActivity import com.android.messaging.ui.UIIntents import com.android.messaging.util.LogUtil import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsScreenEffect as Effect -internal fun handleEffect(context: Context, effect: Effect) { - when (effect) { - is Effect.OpenWirelessAlerts -> { - try { - context.startActivity(UIIntents.get().wirelessAlertsIntent) - } catch (e: ActivityNotFoundException) { - LogUtil.e(LogUtil.BUGLE_TAG, "Failed to launch wireless alerts activity", e) +internal interface SettingsEffectHandler { + fun handle(effect: Effect) +} + +internal class SettingsEffectHandlerImpl( + private val context: Context, +) : SettingsEffectHandler { + + override fun handle(effect: Effect) { + when (effect) { + is Effect.OpenWirelessAlerts -> { + try { + context.startActivity(UIIntents.get().wirelessAlertsIntent) + } catch (e: ActivityNotFoundException) { + LogUtil.e(LogUtil.BUGLE_TAG, "Failed to launch wireless alerts activity", e) + } + } + + is Effect.OpenManageDefaultApps -> { + val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + + is Effect.OpenNotificationSettings -> { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + context.startActivity(intent) + } + + is Effect.RequestDefaultSmsApp -> { + val roleManager = context.getSystemService(RoleManager::class.java) + val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS) + context.startActivity(intent) + } + + is Effect.OpenLicenses -> { + val intent = Intent(context, LicenseActivity::class.java) + context.startActivity(intent) } } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt index 9115c307..07b0ddf9 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.messaging.ui.appsettings.redesign.appsettings.ui.AppSettingsScreen import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsNavRoute import com.android.messaging.ui.appsettings.redesign.subscription.ui.SubscriptionSettingsScreen @@ -40,10 +41,9 @@ internal fun SettingsScreen( screenModel.refreshState() } - LaunchedEffect(screenModel, context) { - screenModel.effects.collect { effect -> - handleEffect(context, effect) - } + val effectHandler = remember(context) { SettingsEffectHandlerImpl(context) } + LaunchedEffect(screenModel, effectHandler) { + screenModel.effects.collect(effectHandler::handle) } // For single-SIM go directly to app settings @@ -108,7 +108,25 @@ internal fun SettingsScreen( ) } - is SettingsNavRoute.AppSettings -> {} + is SettingsNavRoute.AppSettings -> { + AppSettingsScreen( + appSettings = uiState.appSettings, + screenModel = screenModel, + isTopLevel = !uiState.isMultiSim, + onAdvancedClick = uiState.subscriptionSettings + .firstOrNull() + ?.takeIf { !uiState.isMultiSim } + ?.let { sub -> + { + currentRoute = SettingsNavRoute.SubscriptionSettings( + sub.subId, + sub.displayName, + ) + } + }, + onNavigateBack = navigateUp, + ) + } is SettingsNavRoute.SubscriptionSettings -> { val sub = uiState.subscriptionSettings.find { it.subId == route.subId } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt index c619317a..b96b0485 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModel.kt @@ -2,6 +2,7 @@ package com.android.messaging.ui.appsettings.redesign.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.messaging.ui.appsettings.redesign.appsettings.delegate.AppSettingsDelegate import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegate import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow @@ -9,7 +10,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,30 +26,42 @@ internal interface SettingsScreenModel { fun onAutoRetrieveMmsChanged(subId: Int, enabled: Boolean) fun onAutoRetrieveMmsWhenRoamingChanged(subId: Int, enabled: Boolean) fun onDeliveryReportsChanged(subId: Int, enabled: Boolean) - fun onWirelessAlertsClick(subId: Int) fun onGroupMmsChanged(subId: Int, enabled: Boolean) fun onPhoneNumberChanged(subId: Int, phoneNumber: String) + fun onWirelessAlertsClick(subId: Int) + + fun onDumpMmsChanged(enabled: Boolean) + fun onDumpSmsChanged(enabled: Boolean) + fun onSendSoundChanged(enabled: Boolean) + fun onDefaultSmsAppClick(isCurrentlyDefault: Boolean) + fun onNotificationsClick() + + fun onLicensesClick() } @HiltViewModel internal class SettingsViewModel @Inject constructor( private val subscriptionSettingsDelegate: SubscriptionSettingsDelegate, + private val appSettingsDelegate: AppSettingsDelegate, ) : ViewModel(), SettingsScreenModel { private val _effects = MutableSharedFlow(extraBufferCapacity = 1) override val effects: Flow = _effects.asSharedFlow() - override val uiState: StateFlow = subscriptionSettingsDelegate.state - .map { subscriptionState -> - State( - isMultiSim = subscriptionState.isMultiSim, - subscriptionSettings = subscriptionState.subscriptions, - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(STATEFLOW_STOP_TIMEOUT_MILLIS), - initialValue = State(), + override val uiState: StateFlow = combine( + subscriptionSettingsDelegate.state, + appSettingsDelegate.state, + ) { subscriptionState, appSettings -> + State( + isMultiSim = subscriptionState.isMultiSim, + subscriptionSettings = subscriptionState.subscriptions, + appSettings = appSettings, ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(STATEFLOW_STOP_TIMEOUT_MILLIS), + initialValue = State(), + ) init { initializeDelegates() @@ -56,10 +69,12 @@ internal class SettingsViewModel @Inject constructor( private fun initializeDelegates() { subscriptionSettingsDelegate.bind(scope = viewModelScope) + appSettingsDelegate.bind(scope = viewModelScope) } override fun refreshState() { subscriptionSettingsDelegate.refresh() + appSettingsDelegate.refresh() } override fun onAutoRetrieveMmsChanged(subId: Int, enabled: Boolean) { @@ -86,6 +101,35 @@ internal class SettingsViewModel @Inject constructor( emitEffect(Effect.OpenWirelessAlerts(subId)) } + override fun onDumpMmsChanged(enabled: Boolean) { + appSettingsDelegate.onDumpMmsChanged(enabled) + } + + override fun onDumpSmsChanged(enabled: Boolean) { + appSettingsDelegate.onDumpSmsChanged(enabled) + } + + override fun onSendSoundChanged(enabled: Boolean) { + appSettingsDelegate.onSendSoundChanged(enabled) + } + + override fun onDefaultSmsAppClick(isCurrentlyDefault: Boolean) { + val effect = if (isCurrentlyDefault) { + Effect.OpenManageDefaultApps + } else { + Effect.RequestDefaultSmsApp + } + emitEffect(effect) + } + + override fun onNotificationsClick() { + emitEffect(Effect.OpenNotificationSettings) + } + + override fun onLicensesClick() { + emitEffect(Effect.OpenLicenses) + } + private fun emitEffect(effect: Effect) { viewModelScope.launch { _effects.emit(effect) diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt index 573b7bcb..ed0d796c 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsScreenEffect.kt @@ -2,4 +2,8 @@ package com.android.messaging.ui.appsettings.redesign.screen.model internal sealed interface SettingsScreenEffect { data class OpenWirelessAlerts(val subId: Int) : SettingsScreenEffect + data object OpenManageDefaultApps : SettingsScreenEffect + data object RequestDefaultSmsApp : SettingsScreenEffect + data object OpenNotificationSettings : SettingsScreenEffect + data object OpenLicenses : SettingsScreenEffect } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt index 4c56ec1e..e38243a5 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt @@ -1,10 +1,12 @@ package com.android.messaging.ui.appsettings.redesign.screen.model import androidx.compose.runtime.Immutable +import com.android.messaging.ui.appsettings.redesign.appsettings.model.AppSettingsUiState import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState @Immutable internal data class SettingsUiState( val isMultiSim: Boolean = false, val subscriptionSettings: List = emptyList(), + val appSettings: AppSettingsUiState = AppSettingsUiState(), ) From f67d7c2a8273b98901f9be4c4317675f3a261ff0 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Wed, 1 Apr 2026 21:33:15 +0200 Subject: [PATCH 06/11] Fix navigation issues --- .../ui/appsettings/redesign/SettingsActivity.kt | 7 ++++++- .../redesign/screen/SettingsScreen.kt | 17 ++++++++++------- .../redesign/screen/model/SettingsNavRoute.kt | 17 ++++++++++++++--- .../redesign/screen/model/SettingsUiState.kt | 2 +- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt b/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt index a2561a9a..0a06e997 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/SettingsActivity.kt @@ -8,12 +8,17 @@ import com.android.messaging.datamodel.data.ParticipantData import com.android.messaging.ui.UIIntents import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsNavRoute import com.android.messaging.ui.appsettings.redesign.screen.SettingsScreen +import com.android.messaging.ui.appsettings.redesign.subscription.mapper.SubscriptionSettingsUiStateMapper import com.android.messaging.ui.core.AppTheme import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class SettingsActivity : ComponentActivity() { + @Inject + internal lateinit var subscriptionMapper: SubscriptionSettingsUiStateMapper + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -46,7 +51,7 @@ class SettingsActivity : ComponentActivity() { return when { subTitle != null -> SettingsNavRoute.SubscriptionSettings(subId, subTitle) - isTopLevel -> SettingsNavRoute.AppSettings + isTopLevel || !subscriptionMapper.isMultiSim() -> SettingsNavRoute.AppSettings else -> SettingsNavRoute.Main } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt index 07b0ddf9..e6569622 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreen.kt @@ -7,6 +7,8 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -47,14 +49,14 @@ internal fun SettingsScreen( } // For single-SIM go directly to app settings - val effectiveRoute = if (!uiState.isMultiSim && currentRoute is SettingsNavRoute.Main) { + val effectiveRoute = if (uiState.isMultiSim == false && currentRoute is SettingsNavRoute.Main) { SettingsNavRoute.AppSettings } else { currentRoute } val isRootRoute = effectiveRoute is SettingsNavRoute.Main || - (effectiveRoute is SettingsNavRoute.AppSettings && !uiState.isMultiSim) + (effectiveRoute is SettingsNavRoute.AppSettings && uiState.isMultiSim == false) val navigateUp: (() -> Unit) = { when { @@ -65,7 +67,7 @@ internal fun SettingsScreen( } effectiveRoute is SettingsNavRoute.SubscriptionSettings -> { - currentRoute = if (uiState.isMultiSim) { + currentRoute = if (uiState.isMultiSim == true) { SettingsNavRoute.Main } else { SettingsNavRoute.AppSettings @@ -81,9 +83,9 @@ internal fun SettingsScreen( AnimatedContent( targetState = effectiveRoute, - modifier = modifier, + modifier = modifier.background(MaterialTheme.colorScheme.background), transitionSpec = { - val isForward = targetState != SettingsNavRoute.Main + val isForward = targetState.depth > initialState.depth if (isForward) { (slideInHorizontally { it / 3 } + fadeIn()) togetherWith (slideOutHorizontally { -it / 3 } + fadeOut()) @@ -109,13 +111,14 @@ internal fun SettingsScreen( } is SettingsNavRoute.AppSettings -> { + val isSingleSim = uiState.isMultiSim == false AppSettingsScreen( appSettings = uiState.appSettings, screenModel = screenModel, - isTopLevel = !uiState.isMultiSim, + isTopLevel = isSingleSim, onAdvancedClick = uiState.subscriptionSettings .firstOrNull() - ?.takeIf { !uiState.isMultiSim } + ?.takeIf { isSingleSim } ?.let { sub -> { currentRoute = SettingsNavRoute.SubscriptionSettings( diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsNavRoute.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsNavRoute.kt index 2af10fed..3020b7f4 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsNavRoute.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsNavRoute.kt @@ -4,7 +4,18 @@ import androidx.compose.runtime.Immutable @Immutable internal sealed interface SettingsNavRoute { - data object Main : SettingsNavRoute - data object AppSettings : SettingsNavRoute - data class SubscriptionSettings(val subId: Int, val title: String) : SettingsNavRoute + + val depth: Int + + data object Main : SettingsNavRoute { + override val depth: Int = 0 + } + + data object AppSettings : SettingsNavRoute { + override val depth: Int = 1 + } + + data class SubscriptionSettings(val subId: Int, val title: String) : SettingsNavRoute { + override val depth: Int = 2 + } } diff --git a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt index e38243a5..ebc82f97 100644 --- a/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt +++ b/src/com/android/messaging/ui/appsettings/redesign/screen/model/SettingsUiState.kt @@ -6,7 +6,7 @@ import com.android.messaging.ui.appsettings.redesign.subscription.model.Subscrip @Immutable internal data class SettingsUiState( - val isMultiSim: Boolean = false, + val isMultiSim: Boolean? = null, val subscriptionSettings: List = emptyList(), val appSettings: AppSettingsUiState = AppSettingsUiState(), ) From c6c978db794b73286c48d8bcb10b57516f4fef50 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Thu, 2 Apr 2026 22:21:41 +0200 Subject: [PATCH 07/11] Add settings tests --- .../appsettings/ui/AppSettingsScreenTest.kt | 223 +++++++++++ .../redesign/screen/SettingsScreenTest.kt | 192 ++++++++++ .../ui/SubscriptionSettingsScreenTest.kt | 305 +++++++++++++++ .../messaging/testutil/MainDispatcherRule.kt | 23 ++ .../redesign/screen/SettingsViewModelTest.kt | 355 ++++++++++++++++++ 5 files changed, 1098 insertions(+) create mode 100644 app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreenTest.kt create mode 100644 app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreenTest.kt create mode 100644 app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreenTest.kt create mode 100644 app/src/test/java/com/android/messaging/testutil/MainDispatcherRule.kt create mode 100644 app/src/test/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModelTest.kt diff --git a/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreenTest.kt b/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreenTest.kt new file mode 100644 index 00000000..1e215dbe --- /dev/null +++ b/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/appsettings/ui/AppSettingsScreenTest.kt @@ -0,0 +1,223 @@ +package com.android.messaging.ui.appsettings.redesign.appsettings.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.android.messaging.R +import com.android.messaging.ui.appsettings.redesign.appsettings.model.AppSettingsUiState +import com.android.messaging.ui.appsettings.redesign.screen.SettingsScreenModel +import com.android.messaging.ui.core.AppTheme +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class AppSettingsScreenTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + private lateinit var screenModel: SettingsScreenModel + + @Before + fun setup() { + screenModel = mockk(relaxed = true) + } + + @Test + fun defaultSmsAppItem_displaysLabel() { + val appSettings = AppSettingsUiState( + isDefaultSmsApp = true, + defaultSmsAppLabel = "Messaging", + ) + + setContent(appSettings = appSettings) + + composeTestRule.onNodeWithText("Messaging").assertIsDisplayed() + } + + @Test + fun defaultSmsAppClick_delegatesToScreenModel() { + val appSettings = AppSettingsUiState( + isDefaultSmsApp = true, + defaultSmsAppLabel = "Messaging", + ) + + setContent(appSettings = appSettings) + + val title = composeTestRule.activity.getString(R.string.sms_disabled_pref_title) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onDefaultSmsAppClick(true) + } + } + + @Test + fun notificationsClick_delegatesToScreenModel() { + setContent() + + val title = composeTestRule.activity.getString( + R.string.notifications_enabled_conversation_pref_title, + ) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onNotificationsClick() + } + } + + @Test + fun sendSoundToggle_delegatesToScreenModel() { + val appSettings = AppSettingsUiState(sendSoundEnabled = true) + + setContent(appSettings = appSettings) + + val title = composeTestRule.activity.getString(R.string.send_sound_pref_title) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onSendSoundChanged(false) + } + } + + @Test + fun debugSection_hiddenWhenDebugDisabled() { + val appSettings = AppSettingsUiState(isDebugEnabled = false) + + setContent(appSettings = appSettings) + + val debugTitle = composeTestRule.activity.getString(R.string.debug_category_pref_title) + composeTestRule.onNodeWithText(debugTitle).assertDoesNotExist() + + val dumpSmsTitle = composeTestRule.activity.getString(R.string.dump_sms_pref_title) + composeTestRule.onNodeWithText(dumpSmsTitle).assertDoesNotExist() + } + + @Test + fun debugSection_shownWhenDebugEnabled() { + val appSettings = AppSettingsUiState( + isDebugEnabled = true, + dumpSmsEnabled = false, + dumpMmsEnabled = false, + ) + + setContent(appSettings = appSettings) + + val debugTitle = composeTestRule.activity.getString(R.string.debug_category_pref_title) + composeTestRule.onNodeWithText(debugTitle).assertIsDisplayed() + + val dumpSmsTitle = composeTestRule.activity.getString(R.string.dump_sms_pref_title) + composeTestRule.onNodeWithText(dumpSmsTitle).assertIsDisplayed() + + val dumpMmsTitle = composeTestRule.activity.getString(R.string.dump_mms_pref_title) + composeTestRule.onNodeWithText(dumpMmsTitle).assertIsDisplayed() + } + + @Test + fun dumpSmsToggle_delegatesToScreenModel() { + val appSettings = AppSettingsUiState( + isDebugEnabled = true, + dumpSmsEnabled = false, + ) + + setContent(appSettings = appSettings) + + val title = composeTestRule.activity.getString(R.string.dump_sms_pref_title) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onDumpSmsChanged(true) + } + } + + @Test + fun dumpMmsToggle_delegatesToScreenModel() { + val appSettings = AppSettingsUiState( + isDebugEnabled = true, + dumpMmsEnabled = false, + ) + + setContent(appSettings = appSettings) + + val title = composeTestRule.activity.getString(R.string.dump_mms_pref_title) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onDumpMmsChanged(true) + } + } + + @Test + fun licensesClick_delegatesToScreenModel() { + setContent() + + val title = composeTestRule.activity.getString(R.string.menu_license) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onLicensesClick() + } + } + + @Test + fun advancedSettings_shownWhenTopLevel() { + var advancedClicks = 0 + + composeTestRule.setContent { + AppTheme { + AppSettingsScreen( + appSettings = AppSettingsUiState(), + screenModel = screenModel, + onNavigateBack = {}, + isTopLevel = true, + onAdvancedClick = { advancedClicks += 1 }, + ) + } + } + + val advancedTitle = composeTestRule.activity.getString(R.string.advanced_settings) + composeTestRule.onNodeWithText(advancedTitle).assertIsDisplayed() + composeTestRule.onNodeWithText(advancedTitle).performClick() + + composeTestRule.runOnIdle { + assertEquals(1, advancedClicks) + } + } + + @Test + fun advancedSettings_hiddenWhenNotTopLevel() { + composeTestRule.setContent { + AppTheme { + AppSettingsScreen( + appSettings = AppSettingsUiState(), + screenModel = screenModel, + onNavigateBack = {}, + isTopLevel = false, + onAdvancedClick = null, + ) + } + } + + val advancedTitle = composeTestRule.activity.getString(R.string.advanced_settings) + composeTestRule.onNodeWithText(advancedTitle).assertDoesNotExist() + } + + private fun setContent( + appSettings: AppSettingsUiState = AppSettingsUiState(), + ) { + composeTestRule.setContent { + AppTheme { + AppSettingsScreen( + appSettings = appSettings, + screenModel = screenModel, + onNavigateBack = {}, + ) + } + } + } +} diff --git a/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreenTest.kt b/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreenTest.kt new file mode 100644 index 00000000..e0e10108 --- /dev/null +++ b/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsScreenTest.kt @@ -0,0 +1,192 @@ +package com.android.messaging.ui.appsettings.redesign.screen + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.android.messaging.R +import com.android.messaging.ui.appsettings.redesign.appsettings.model.AppSettingsUiState +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsNavRoute +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsUiState +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState +import com.android.messaging.ui.core.AppTheme +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class SettingsScreenTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + private val fakeUiStateFlow = MutableStateFlow(createSingleSimState()) + private lateinit var screenModel: SettingsScreenModel + + @Before + fun setup() { + screenModel = mockk(relaxed = true) + every { screenModel.uiState } returns fakeUiStateFlow + } + + @Test + fun singleSim_skipsMainScreen_showsAppSettings() { + fakeUiStateFlow.value = createSingleSimState() + + setScreenContent() + + val generalTitle = composeTestRule.activity.getString(R.string.settings_activity_title) + composeTestRule.onNodeWithText(generalTitle).assertIsDisplayed() + + val sendSoundTitle = composeTestRule.activity.getString(R.string.send_sound_pref_title) + composeTestRule.onNodeWithText(sendSoundTitle).assertIsDisplayed() + } + + @Test + fun multiSim_showsMainScreen_withSubscriptions() { + fakeUiStateFlow.value = createMultiSimState() + + setScreenContent() + + val settingsTitle = composeTestRule.activity.getString(R.string.settings_activity_title) + composeTestRule.onNodeWithText(settingsTitle).assertIsDisplayed() + + composeTestRule.onNodeWithText("SIM 1").assertIsDisplayed() + composeTestRule.onNodeWithText("SIM 2").assertIsDisplayed() + } + + @Test + fun multiSim_generalSettingsClick_navigatesToAppSettings() { + fakeUiStateFlow.value = createMultiSimState() + + setScreenContent() + + val generalSettings = composeTestRule.activity.getString(R.string.general_settings) + composeTestRule.onNodeWithText(generalSettings).performClick() + composeTestRule.waitForIdle() + + val sendSoundTitle = composeTestRule.activity.getString(R.string.send_sound_pref_title) + composeTestRule.onNodeWithText(sendSoundTitle).assertIsDisplayed() + } + + @Test + fun lifecycleResume_refreshesState() { + fakeUiStateFlow.value = createSingleSimState() + lateinit var lifecycleOwner: TestLifecycleOwner + + composeTestRule.runOnIdle { + lifecycleOwner = TestLifecycleOwner( + initialState = Lifecycle.State.STARTED, + ) + } + + composeTestRule.setContent { + CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { + AppTheme { + SettingsScreen( + onNavigateBack = {}, + screenModel = screenModel, + ) + } + } + } + + composeTestRule.runOnIdle { + lifecycleOwner.moveTo(state = Lifecycle.State.RESUMED) + } + composeTestRule.waitForIdle() + + verify(atLeast = 1) { + screenModel.refreshState() + } + } + + @Test + fun singleSim_showsAdvancedSettings() { + fakeUiStateFlow.value = createSingleSimState() + + setScreenContent() + + val advancedTitle = composeTestRule.activity.getString(R.string.advanced_settings) + composeTestRule.onNodeWithText(advancedTitle).assertIsDisplayed() + } + + private fun setScreenContent( + initialRoute: SettingsNavRoute = SettingsNavRoute.Main, + ) { + composeTestRule.setContent { + AppTheme { + SettingsScreen( + onNavigateBack = {}, + initialRoute = initialRoute, + screenModel = screenModel, + ) + } + } + } + + private fun createSingleSimState(): SettingsUiState { + return SettingsUiState( + appSettings = AppSettingsUiState( + isDefaultSmsApp = true, + defaultSmsAppLabel = "Messaging", + sendSoundEnabled = true, + ), + subscriptionSettings = listOf( + SubscriptionSettingsUiState( + subId = 1, + displayName = "Advanced Settings", + displayDetail = "+1234567890", + ), + ), + isMultiSim = false, + ) + } + + private fun createMultiSimState(): SettingsUiState { + return SettingsUiState( + appSettings = AppSettingsUiState( + isDefaultSmsApp = true, + defaultSmsAppLabel = "Messaging", + sendSoundEnabled = true, + ), + subscriptionSettings = listOf( + SubscriptionSettingsUiState( + subId = 1, + displayName = "SIM 1", + displayDetail = "+1234567890", + ), + SubscriptionSettingsUiState( + subId = 2, + displayName = "SIM 2", + displayDetail = "+0987654321", + ), + ), + isMultiSim = true, + ) + } + + private class TestLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry(this) + + init { + lifecycleRegistry.currentState = initialState + } + + override val lifecycle: Lifecycle + get() = lifecycleRegistry + + fun moveTo(state: Lifecycle.State) { + lifecycleRegistry.currentState = state + } + } +} diff --git a/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreenTest.kt b/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreenTest.kt new file mode 100644 index 00000000..4949ba40 --- /dev/null +++ b/app/src/androidTest/java/com/android/messaging/ui/appsettings/redesign/subscription/ui/SubscriptionSettingsScreenTest.kt @@ -0,0 +1,305 @@ +package com.android.messaging.ui.appsettings.redesign.subscription.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.android.messaging.R +import com.android.messaging.ui.appsettings.redesign.screen.SettingsScreenModel +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState +import com.android.messaging.ui.core.AppTheme +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class SubscriptionSettingsScreenTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + private lateinit var screenModel: SettingsScreenModel + + @Before + fun setup() { + screenModel = mockk(relaxed = true) + } + + @Test + fun mmsCategoryHeader_isDisplayed() { + setContent(subscriptionSettings = createDefaultSubscription()) + + val mmsTitle = composeTestRule.activity.getString( + R.string.mms_messaging_category_pref_title, + ) + composeTestRule.onNodeWithText(mmsTitle).assertIsDisplayed() + } + + @Test + fun groupMms_shownWhenSupported() { + val sub = createDefaultSubscription(isGroupMmsSupported = true) + setContent(subscriptionSettings = sub) + + val groupMmsTitle = composeTestRule.activity.getString(R.string.group_mms_pref_title) + composeTestRule.onNodeWithText(groupMmsTitle).assertIsDisplayed() + } + + @Test + fun groupMms_hiddenWhenNotSupported() { + val sub = createDefaultSubscription(isGroupMmsSupported = false) + setContent(subscriptionSettings = sub) + + val groupMmsTitle = composeTestRule.activity.getString(R.string.group_mms_pref_title) + composeTestRule.onNodeWithText(groupMmsTitle).assertDoesNotExist() + } + + @Test + fun groupMmsClick_showsDialog() { + val sub = createDefaultSubscription( + isGroupMmsSupported = true, + isDefaultSmsApp = true, + ) + setContent(subscriptionSettings = sub) + + val groupMmsTitle = composeTestRule.activity.getString(R.string.group_mms_pref_title) + composeTestRule.onNodeWithText(groupMmsTitle).performClick() + composeTestRule.waitForIdle() + + val disableLabel = composeTestRule.activity.getString(R.string.disable_group_mms) + composeTestRule.onNodeWithText(disableLabel).assertIsDisplayed() + + val okText = composeTestRule.activity.getString(android.R.string.ok) + composeTestRule.onNodeWithText(okText).assertIsDisplayed() + + val cancelText = composeTestRule.activity.getString(android.R.string.cancel) + composeTestRule.onNodeWithText(cancelText).assertIsDisplayed() + } + + @Test + fun phoneNumberItem_displaysCurrentNumber() { + val sub = createDefaultSubscription(displayDetail = "+1234567890") + setContent(subscriptionSettings = sub) + + composeTestRule.onNodeWithText("+1234567890").assertIsDisplayed() + } + + @Test + fun phoneNumberClick_showsDialog() { + val sub = createDefaultSubscription(phoneNumber = "+1234567890") + setContent(subscriptionSettings = sub) + + val phoneTitle = composeTestRule.activity.getString(R.string.mms_phone_number_pref_title) + composeTestRule.onNodeWithText(phoneTitle).performClick() + composeTestRule.waitForIdle() + + val okText = composeTestRule.activity.getString(android.R.string.ok) + composeTestRule.onNodeWithText(okText).assertIsDisplayed() + } + + @Test + fun autoRetrieveMms_toggleDelegatesToScreenModel() { + val sub = createDefaultSubscription( + isDefaultSmsApp = true, + autoRetrieveMms = true, + ) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString(R.string.auto_retrieve_mms_pref_title) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onAutoRetrieveMmsChanged(1, false) + } + } + + @Test + fun autoRetrieveMmsWhenRoaming_disabledWhenAutoRetrieveOff() { + val sub = createDefaultSubscription( + isDefaultSmsApp = true, + autoRetrieveMms = false, + ) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString( + R.string.auto_retrieve_mms_when_roaming_pref_title, + ) + composeTestRule.onNodeWithText(title).assertIsNotEnabled() + } + + @Test + fun autoRetrieveMmsWhenRoaming_enabledWhenAutoRetrieveOn() { + val sub = createDefaultSubscription( + isDefaultSmsApp = true, + autoRetrieveMms = true, + ) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString( + R.string.auto_retrieve_mms_when_roaming_pref_title, + ) + composeTestRule.onNodeWithText(title).assertIsEnabled() + } + + @Test + fun deliveryReports_shownWhenSupported() { + val sub = createDefaultSubscription(isDeliveryReportsSupported = true) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString(R.string.delivery_reports_pref_title) + composeTestRule.onNodeWithText(title).assertIsDisplayed() + } + + @Test + fun deliveryReports_hiddenWhenNotSupported() { + val sub = createDefaultSubscription(isDeliveryReportsSupported = false) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString(R.string.delivery_reports_pref_title) + composeTestRule.onNodeWithText(title).assertDoesNotExist() + } + + @Test + fun deliveryReportsToggle_delegatesToScreenModel() { + val sub = createDefaultSubscription( + isDeliveryReportsSupported = true, + isDefaultSmsApp = true, + deliveryReportsEnabled = false, + ) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString(R.string.delivery_reports_pref_title) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onDeliveryReportsChanged(1, true) + } + } + + @Test + fun wirelessAlerts_shownWhenSupported() { + val sub = createDefaultSubscription(isWirelessAlertsSupported = true) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString(R.string.wireless_alerts_title) + composeTestRule.onNodeWithText(title).assertIsDisplayed() + } + + @Test + fun wirelessAlerts_hiddenWhenNotSupported() { + val sub = createDefaultSubscription(isWirelessAlertsSupported = false) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString(R.string.wireless_alerts_title) + composeTestRule.onNodeWithText(title).assertDoesNotExist() + } + + @Test + fun wirelessAlertsClick_delegatesToScreenModel() { + val sub = createDefaultSubscription(isWirelessAlertsSupported = true) + setContent(subscriptionSettings = sub) + + val title = composeTestRule.activity.getString(R.string.wireless_alerts_title) + composeTestRule.onNodeWithText(title).performClick() + + verify(exactly = 1) { + screenModel.onWirelessAlertsClick(1) + } + } + + @Test + fun advancedCategory_shownWhenDeliveryReportsOrWirelessAlertsSupported() { + val sub = createDefaultSubscription( + isDeliveryReportsSupported = true, + isWirelessAlertsSupported = false, + ) + setContent(subscriptionSettings = sub) + + val advancedTitle = + composeTestRule.activity.getString(R.string.advanced_category_pref_title) + composeTestRule.onNodeWithText(advancedTitle).assertIsDisplayed() + } + + @Test + fun advancedCategory_hiddenWhenNeitherSupported() { + val sub = createDefaultSubscription( + isDeliveryReportsSupported = false, + isWirelessAlertsSupported = false, + ) + setContent(subscriptionSettings = sub) + + val advancedTitle = + composeTestRule.activity.getString(R.string.advanced_category_pref_title) + composeTestRule.onNodeWithText(advancedTitle).assertDoesNotExist() + } + + @Test + fun settingsDisabled_whenNotDefaultSmsApp() { + val sub = createDefaultSubscription( + isDefaultSmsApp = false, + isGroupMmsSupported = true, + isDeliveryReportsSupported = true, + ) + setContent(subscriptionSettings = sub) + + val groupMmsTitle = composeTestRule.activity.getString(R.string.group_mms_pref_title) + composeTestRule.onNodeWithText(groupMmsTitle).assertIsNotEnabled() + + val autoRetrieveTitle = composeTestRule.activity.getString( + R.string.auto_retrieve_mms_pref_title, + ) + composeTestRule.onNodeWithText(autoRetrieveTitle).assertIsNotEnabled() + + val deliveryTitle = composeTestRule.activity.getString(R.string.delivery_reports_pref_title) + composeTestRule.onNodeWithText(deliveryTitle).assertIsNotEnabled() + } + + private fun setContent( + subscriptionSettings: SubscriptionSettingsUiState = createDefaultSubscription(), + ) { + composeTestRule.setContent { + AppTheme { + SubscriptionSettingsScreen( + subscriptionSettings = subscriptionSettings, + title = "Advanced Settings", + screenModel = screenModel, + onNavigateBack = {}, + ) + } + } + } + + private fun createDefaultSubscription( + subId: Int = 1, + displayDetail: String = "+1234567890", + phoneNumber: String = "+1234567890", + defaultPhoneNumber: String = "+1234567890", + isGroupMmsSupported: Boolean = false, + isGroupMmsEnabled: Boolean = true, + autoRetrieveMms: Boolean = true, + autoRetrieveMmsWhenRoaming: Boolean = false, + isDeliveryReportsSupported: Boolean = false, + deliveryReportsEnabled: Boolean = false, + isWirelessAlertsSupported: Boolean = false, + isDefaultSmsApp: Boolean = true, + ): SubscriptionSettingsUiState { + return SubscriptionSettingsUiState( + subId = subId, + displayName = "SIM 1", + displayDetail = displayDetail, + phoneNumber = phoneNumber, + defaultPhoneNumber = defaultPhoneNumber, + isGroupMmsSupported = isGroupMmsSupported, + isGroupMmsEnabled = isGroupMmsEnabled, + autoRetrieveMms = autoRetrieveMms, + autoRetrieveMmsWhenRoaming = autoRetrieveMmsWhenRoaming, + isDeliveryReportsSupported = isDeliveryReportsSupported, + deliveryReportsEnabled = deliveryReportsEnabled, + isWirelessAlertsSupported = isWirelessAlertsSupported, + isDefaultSmsApp = isDefaultSmsApp, + ) + } +} diff --git a/app/src/test/java/com/android/messaging/testutil/MainDispatcherRule.kt b/app/src/test/java/com/android/messaging/testutil/MainDispatcherRule.kt new file mode 100644 index 00000000..9b2a0bda --- /dev/null +++ b/app/src/test/java/com/android/messaging/testutil/MainDispatcherRule.kt @@ -0,0 +1,23 @@ +package com.android.messaging.testutil + +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 testDispatcher: TestDispatcher = StandardTestDispatcher()) : + TestWatcher() { + + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} diff --git a/app/src/test/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModelTest.kt b/app/src/test/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModelTest.kt new file mode 100644 index 00000000..20e24203 --- /dev/null +++ b/app/src/test/java/com/android/messaging/ui/appsettings/redesign/screen/SettingsViewModelTest.kt @@ -0,0 +1,355 @@ +package com.android.messaging.ui.appsettings.redesign.screen + +import app.cash.turbine.test +import com.android.messaging.testutil.MainDispatcherRule +import com.android.messaging.ui.appsettings.redesign.appsettings.delegate.AppSettingsDelegate +import com.android.messaging.ui.appsettings.redesign.appsettings.model.AppSettingsUiState +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsScreenEffect +import com.android.messaging.ui.appsettings.redesign.screen.model.SettingsUiState +import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsDelegate +import com.android.messaging.ui.appsettings.redesign.subscription.delegate.SubscriptionSettingsState +import com.android.messaging.ui.appsettings.redesign.subscription.model.SubscriptionSettingsUiState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class SettingsViewModelTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun init_bindsAllDelegates() { + runTest(context = mainDispatcherRule.testDispatcher) { + val appDelegate = FakeAppSettingsDelegate() + val subDelegate = FakeSubscriptionSettingsDelegate() + + createViewModel( + appSettingsDelegate = appDelegate, + subscriptionSettingsDelegate = subDelegate, + ) + advanceUntilIdle() + + assertEquals(1, appDelegate.bindCalls) + assertEquals(1, subDelegate.bindCalls) + } + } + + @Test + fun uiState_combinesDelegateStates() { + runTest(context = mainDispatcherRule.testDispatcher) { + val appDelegate = FakeAppSettingsDelegate() + val subDelegate = FakeSubscriptionSettingsDelegate() + val viewModel = createViewModel( + appSettingsDelegate = appDelegate, + subscriptionSettingsDelegate = subDelegate, + ) + + val appState = AppSettingsUiState( + isDefaultSmsApp = true, + defaultSmsAppLabel = "Messaging", + sendSoundEnabled = false, + ) + val subscription = SubscriptionSettingsUiState( + subId = 1, + displayName = "SIM 1", + ) + appDelegate.stateFlow.value = appState + subDelegate.stateFlow.value = SubscriptionSettingsState( + subscriptions = listOf(subscription), + isMultiSim = false, + ) + + viewModel.uiState.test { + assertEquals(SettingsUiState(), awaitItem()) + + val mappedState = awaitItem() + assertEquals(appState, mappedState.appSettings) + assertEquals(listOf(subscription), mappedState.subscriptionSettings) + assertEquals(false, mappedState.isMultiSim) + cancelAndIgnoreRemainingEvents() + } + } + } + + @Test + fun refreshState_refreshesBothDelegates() { + runTest(context = mainDispatcherRule.testDispatcher) { + val appDelegate = FakeAppSettingsDelegate() + val subDelegate = FakeSubscriptionSettingsDelegate() + val viewModel = createViewModel( + appSettingsDelegate = appDelegate, + subscriptionSettingsDelegate = subDelegate, + ) + + viewModel.refreshState() + + assertEquals(1, appDelegate.refreshCalls) + assertEquals(1, subDelegate.refreshCalls) + } + } + + @Test + fun onSendSoundChanged_delegatesToAppSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val appDelegate = FakeAppSettingsDelegate() + val viewModel = createViewModel(appSettingsDelegate = appDelegate) + + viewModel.onSendSoundChanged(enabled = false) + + assertEquals(listOf(false), appDelegate.sendSoundChanges) + } + } + + @Test + fun onDumpSmsChanged_delegatesToAppSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val appDelegate = FakeAppSettingsDelegate() + val viewModel = createViewModel(appSettingsDelegate = appDelegate) + + viewModel.onDumpSmsChanged(enabled = true) + + assertEquals(listOf(true), appDelegate.dumpSmsChanges) + } + } + + @Test + fun onDumpMmsChanged_delegatesToAppSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val appDelegate = FakeAppSettingsDelegate() + val viewModel = createViewModel(appSettingsDelegate = appDelegate) + + viewModel.onDumpMmsChanged(enabled = true) + + assertEquals(listOf(true), appDelegate.dumpMmsChanges) + } + } + + @Test + fun onGroupMmsChanged_delegatesToSubscriptionSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val subDelegate = FakeSubscriptionSettingsDelegate() + val viewModel = createViewModel(subscriptionSettingsDelegate = subDelegate) + + viewModel.onGroupMmsChanged(subId = 1, enabled = false) + + assertEquals(listOf(1 to false), subDelegate.groupMmsChanges) + } + } + + @Test + fun onPhoneNumberChanged_delegatesToSubscriptionSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val subDelegate = FakeSubscriptionSettingsDelegate() + val viewModel = createViewModel(subscriptionSettingsDelegate = subDelegate) + + viewModel.onPhoneNumberChanged(subId = 1, phoneNumber = "+1555000111") + + assertEquals(listOf(1 to "+1555000111"), subDelegate.phoneNumberChanges) + } + } + + @Test + fun onAutoRetrieveMmsChanged_delegatesToSubscriptionSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val subDelegate = FakeSubscriptionSettingsDelegate() + val viewModel = createViewModel(subscriptionSettingsDelegate = subDelegate) + + viewModel.onAutoRetrieveMmsChanged(subId = 2, enabled = true) + + assertEquals(listOf(2 to true), subDelegate.autoRetrieveMmsChanges) + } + } + + @Test + fun onAutoRetrieveMmsWhenRoamingChanged_delegatesToSubscriptionSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val subDelegate = FakeSubscriptionSettingsDelegate() + val viewModel = createViewModel(subscriptionSettingsDelegate = subDelegate) + + viewModel.onAutoRetrieveMmsWhenRoamingChanged(subId = 1, enabled = true) + + assertEquals(listOf(1 to true), subDelegate.autoRetrieveMmsWhenRoamingChanges) + } + } + + @Test + fun onDeliveryReportsChanged_delegatesToSubscriptionSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val subDelegate = FakeSubscriptionSettingsDelegate() + val viewModel = createViewModel(subscriptionSettingsDelegate = subDelegate) + + viewModel.onDeliveryReportsChanged(subId = 1, enabled = true) + + assertEquals(listOf(1 to true), subDelegate.deliveryReportsChanges) + } + } + + @Test + fun onDefaultSmsAppClick_whenDefault_emitsOpenManageDefaultApps() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.effects.test { + viewModel.onDefaultSmsAppClick(isCurrentlyDefault = true) + + assertEquals(SettingsScreenEffect.OpenManageDefaultApps, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + } + + @Test + fun onDefaultSmsAppClick_whenNotDefault_emitsRequestDefaultSmsApp() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.effects.test { + viewModel.onDefaultSmsAppClick(isCurrentlyDefault = false) + + assertEquals(SettingsScreenEffect.RequestDefaultSmsApp, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + } + + @Test + fun onNotificationsClick_emitsOpenNotificationSettings() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.effects.test { + viewModel.onNotificationsClick() + + assertEquals(SettingsScreenEffect.OpenNotificationSettings, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + } + + @Test + fun onWirelessAlertsClick_emitsOpenWirelessAlerts() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.effects.test { + viewModel.onWirelessAlertsClick(subId = 1) + + assertEquals(SettingsScreenEffect.OpenWirelessAlerts(subId = 1), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + } + + @Test + fun onLicensesClick_emitsOpenLicenses() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.effects.test { + viewModel.onLicensesClick() + + assertEquals(SettingsScreenEffect.OpenLicenses, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + } + + private fun createViewModel( + appSettingsDelegate: AppSettingsDelegate = FakeAppSettingsDelegate(), + subscriptionSettingsDelegate: SubscriptionSettingsDelegate = + FakeSubscriptionSettingsDelegate(), + ): SettingsViewModel { + return SettingsViewModel( + appSettingsDelegate = appSettingsDelegate, + subscriptionSettingsDelegate = subscriptionSettingsDelegate, + ) + } + + private class FakeAppSettingsDelegate : AppSettingsDelegate { + val stateFlow = MutableStateFlow(AppSettingsUiState()) + override val state: StateFlow = stateFlow + + var bindCalls = 0 + var refreshCalls = 0 + val sendSoundChanges = mutableListOf() + val dumpSmsChanges = mutableListOf() + val dumpMmsChanges = mutableListOf() + + override fun bind(scope: CoroutineScope) { + bindCalls += 1 + } + + override fun refresh() { + refreshCalls += 1 + } + + override fun onSendSoundChanged(enabled: Boolean) { + sendSoundChanges += enabled + } + + override fun onDumpSmsChanged(enabled: Boolean) { + dumpSmsChanges += enabled + } + + override fun onDumpMmsChanged(enabled: Boolean) { + dumpMmsChanges += enabled + } + } + + private class FakeSubscriptionSettingsDelegate : SubscriptionSettingsDelegate { + val stateFlow = MutableStateFlow(SubscriptionSettingsState()) + override val state: StateFlow = stateFlow + + var bindCalls = 0 + var refreshCalls = 0 + val groupMmsChanges = mutableListOf>() + val phoneNumberChanges = mutableListOf>() + val autoRetrieveMmsChanges = mutableListOf>() + val autoRetrieveMmsWhenRoamingChanges = mutableListOf>() + val deliveryReportsChanges = mutableListOf>() + + override fun bind(scope: CoroutineScope) { + bindCalls += 1 + } + + override fun refresh() { + refreshCalls += 1 + } + + override fun onGroupMmsChanged(subId: Int, enabled: Boolean) { + groupMmsChanges += subId to enabled + } + + override fun onPhoneNumberChanged(subId: Int, phoneNumber: String) { + phoneNumberChanges += subId to phoneNumber + } + + override fun onAutoRetrieveMmsChanged(subId: Int, enabled: Boolean) { + autoRetrieveMmsChanges += subId to enabled + } + + override fun onAutoRetrieveMmsWhenRoamingChanged(subId: Int, enabled: Boolean) { + autoRetrieveMmsWhenRoamingChanges += subId to enabled + } + + override fun onDeliveryReportsChanged(subId: Int, enabled: Boolean) { + deliveryReportsChanges += subId to enabled + } + } +} From 046dd9e64bb6a629476ef54f6670b73771892d86 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Fri, 3 Apr 2026 01:02:52 +0200 Subject: [PATCH 08/11] Add conversation user flow test --- app/build.gradle.kts | 5 + .../conversation/ConversationUserFlowTest.kt | 76 + gradle/libs.versions.toml | 10 + gradle/verification-metadata.xml | 3065 ++++++++--------- 4 files changed, 1436 insertions(+), 1720 deletions(-) create mode 100644 app/src/androidTest/java/com/android/messaging/ui/appsettings/conversation/ConversationUserFlowTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a2f88b5..4ba8d3b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -168,6 +168,11 @@ dependencies { androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.test.espresso.contrib) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.hilt.android.testing) kspAndroidTest(libs.hilt.compiler) diff --git a/app/src/androidTest/java/com/android/messaging/ui/appsettings/conversation/ConversationUserFlowTest.kt b/app/src/androidTest/java/com/android/messaging/ui/appsettings/conversation/ConversationUserFlowTest.kt new file mode 100644 index 00000000..a65d5e63 --- /dev/null +++ b/app/src/androidTest/java/com/android/messaging/ui/appsettings/conversation/ConversationUserFlowTest.kt @@ -0,0 +1,76 @@ +package com.android.messaging.ui.appsettings.conversation + +import android.os.ParcelFileDescriptor +import android.widget.EditText +import android.widget.ImageButton +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.messaging.R +import com.android.messaging.debug.seedTestData +import com.android.messaging.ui.conversationlist.ConversationListActivity +import org.hamcrest.Matchers.allOf +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ConversationUserFlowTest { + + @Before + fun setUpDefaultSmsApp() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val packageName = instrumentation.targetContext.packageName + val command = "cmd role add-role-holder android.app.role.SMS $packageName" + val parcelFileDescriptor = instrumentation.uiAutomation.executeShellCommand(command) + + ParcelFileDescriptor.AutoCloseInputStream(parcelFileDescriptor).use { inputStream -> + val result = String(inputStream.readBytes()) + println("Role assignment result: $result") + } + } + + @Test + fun conversationListToConversation_uiElementsArePresent() { + val scenario = ActivityScenario.launch( + ConversationListActivity::class.java, + ) + + onView(withId(android.R.id.list)) + .check(matches(isDisplayed())) + + onView(withId(R.id.start_new_conversation_button)) + .check(matches(isDisplayed())) + + onView(withId(android.R.id.list)) + .perform( + RecyclerViewActions.actionOnItemAtPosition(0, click()), + ) + + onView(allOf(withId(R.id.compose_message_text), isAssignableFrom(EditText::class.java))) + .check(matches(isDisplayed())) + + onView(allOf(withId(R.id.attach_media_button), isAssignableFrom(ImageButton::class.java))) + .check(matches(isDisplayed())) + + scenario.close() + } + + companion object { + @JvmStatic + @BeforeClass + fun seedOnce() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + seedTestData(context) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99fe9844..6cfa7ff0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,10 @@ mockk = "1.14.9" robolectric = "4.16.1" turbine = "1.2.1" +androidx-test-espresso = "3.7.0" +androidx-test-ext-junit = "1.3.0" +androidx-test-runner = "1.7.0" + [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } @@ -77,6 +81,12 @@ hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } ksp-gradle-plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" } +androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "androidx-test-espresso" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-runner" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 43d519d4..9dfa65f0 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6,15 +6,15 @@ - - - + + + @@ -22,51 +22,51 @@ - - - + + + - - - + + + - - - - - - + + + + + + - - - + + + @@ -79,11 +79,16 @@ + + + - - + + + + @@ -97,9 +102,6 @@ - - - @@ -110,26 +112,26 @@ - - - - - - + + + + + + @@ -140,138 +142,140 @@ - - - - - - + + + - - - + + + - - - + + + - - - - - - + + + + + + - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + + + + - - - + + + @@ -279,36 +283,27 @@ - - - - - - - - - + + + - - - @@ -319,23 +314,20 @@ - - - - - - + + + @@ -348,18 +340,18 @@ - - - - - - + + + + + + @@ -367,18 +359,18 @@ - - - - - - + + + + + + @@ -386,18 +378,18 @@ - - - - - - + + + + + + @@ -405,18 +397,18 @@ - - - - - - + + + + + + @@ -424,18 +416,18 @@ - - - - - - + + + + + + @@ -443,18 +435,18 @@ - - - - - - + + + + + + @@ -470,15 +462,15 @@ - - - + + + @@ -494,15 +486,15 @@ - - - + + + @@ -510,18 +502,18 @@ - - - - - - + + + + + + @@ -529,18 +521,18 @@ - - - - - - + + + + + + @@ -548,37 +540,37 @@ - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + @@ -586,18 +578,18 @@ - - - - - - + + + + + + @@ -605,18 +597,18 @@ - - - - - - + + + + + + @@ -624,15 +616,15 @@ - - - + + + @@ -640,18 +632,18 @@ - - - - - - + + + + + + @@ -659,18 +651,18 @@ - - - - - - + + + + + + @@ -678,26 +670,26 @@ - - - + + + - - - + + + @@ -705,18 +697,18 @@ - - - - - - + + + + + + @@ -724,18 +716,18 @@ - - - - - - + + + + + + @@ -743,15 +735,15 @@ - - - + + + @@ -759,15 +751,15 @@ - - - + + + @@ -775,18 +767,18 @@ - - - - - - + + + + + + @@ -794,119 +786,151 @@ - - - + + + - - - - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -915,53 +939,50 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -970,100 +991,97 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - + + + - - - + + + - - - + + + - - - + + + @@ -1071,26 +1089,26 @@ - - - + + + - - - + + + @@ -1098,42 +1116,39 @@ - - - + + + - - - + + + - - - + + + - - - @@ -1152,64 +1167,58 @@ - - - - - - + + + - - - - - - + + + - - - + + + - - - + + + @@ -1220,15 +1229,15 @@ - - - + + + @@ -1236,56 +1245,53 @@ - - - - - - + + + - - - + + + - - - + + + - - - + + + @@ -1293,23 +1299,20 @@ - - - - - - + + + @@ -1317,42 +1320,36 @@ - - - - - - + + + - - - - - - + + + @@ -1360,18 +1357,18 @@ - - - - - - + + + + + + @@ -1379,15 +1376,15 @@ - - - + + + @@ -1403,42 +1400,36 @@ - - - - - - + + + - - - - - - + + + @@ -1446,29 +1437,29 @@ - - - - - - + + + + + + - - - + + + @@ -1481,53 +1472,47 @@ - - - - - - + + + - - - - - - + + + + + + - - - + + + - - - @@ -1536,15 +1521,15 @@ - - - + + + @@ -1552,18 +1537,18 @@ - - - - - - + + + + + + @@ -1571,45 +1556,42 @@ - - - - - - + + + + + + - - - + + + - - - + + + - - - @@ -1618,67 +1600,64 @@ - - - + + + - - - - - - + + + - - - + + + - - - + + + - - - + + + @@ -1686,18 +1665,18 @@ - - - - - - + + + + + + @@ -1715,34 +1694,25 @@ - - - - - - - - - - - - + + + + + + - - - @@ -1753,301 +1723,291 @@ - - - - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - + + + - - + + - - + + - - - - - - - + + - - + + + + - - + + - - + + - - - - - - + + + - - + + - - + + - - - + + + - - + + - - + + - - + + + + + + + + + + - - - - - - - + + - - + + + + - - + + - - + + - - - + + + - - + + - - - - - + + - - - + + + - - - + + + + + + + + + + + + + + - - - + + + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + @@ -2055,18 +2015,18 @@ - - - - - - + + + + + + @@ -2074,15 +2034,15 @@ - - - + + + @@ -2106,40 +2066,40 @@ - - - - - - + + + + + + - - - + + + - - - + + + @@ -2147,48 +2107,48 @@ - - - + + + - - - + + + - - - + + + - - - + + + @@ -2199,86 +2159,83 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -2287,81 +2244,81 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + @@ -2372,97 +2329,94 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - @@ -2495,15 +2449,15 @@ - - - + + + @@ -2538,31 +2492,28 @@ - - - + + + - - - + + + - - - @@ -2571,9 +2522,6 @@ - - - @@ -2582,9 +2530,6 @@ - - - @@ -2593,9 +2538,6 @@ - - - @@ -2604,9 +2546,6 @@ - - - @@ -2615,9 +2554,6 @@ - - - @@ -2626,9 +2562,6 @@ - - - @@ -2637,9 +2570,6 @@ - - - @@ -2648,9 +2578,6 @@ - - - @@ -2659,9 +2586,6 @@ - - - @@ -2670,9 +2594,6 @@ - - - @@ -2681,9 +2602,6 @@ - - - @@ -2692,9 +2610,6 @@ - - - @@ -2703,9 +2618,6 @@ - - - @@ -2714,9 +2626,6 @@ - - - @@ -2725,9 +2634,6 @@ - - - @@ -2736,9 +2642,6 @@ - - - @@ -2747,20 +2650,17 @@ - - - + + + - - - @@ -2782,79 +2682,78 @@ - - - + + + - - - - - - - - - - + + - - + + + + + + + + + + + + + + + + - - - - - + + + + - - - - - - - - - - + + - - + + + + @@ -2862,6 +2761,25 @@ + + + + + + + + + + + + + + + + + + + @@ -2871,9 +2789,6 @@ - - - @@ -2882,9 +2797,6 @@ - - - @@ -2898,9 +2810,6 @@ - - - @@ -2914,9 +2823,6 @@ - - - @@ -2925,29 +2831,29 @@ - - - - - - + + + + + + - - - + + + @@ -2960,18 +2866,18 @@ - - - - - - + + + + + + @@ -2982,26 +2888,17 @@ - - - - - - + + + - - - - - - @@ -3025,9 +2922,6 @@ - - - @@ -3036,31 +2930,28 @@ - - - + + + - - - + + + - - - @@ -3069,18 +2960,18 @@ - - - - - - + + + + + + @@ -3107,43 +2998,43 @@ - - - - - - + + + + + + - - - + + + - - - - - - + + + + + + @@ -3154,18 +3045,18 @@ - - - - - - + + + + + + @@ -3199,51 +3090,51 @@ - - - + + + - - - + + + - - - + + + - - - - - - + + + + + + @@ -3259,34 +3150,22 @@ - - - - - - - - - + + + - - - - - - @@ -3295,18 +3174,18 @@ - - - - - - + + + + + + @@ -3339,15 +3218,15 @@ - - - + + + @@ -3362,13 +3241,7 @@ - - - - - - - + @@ -3377,32 +3250,29 @@ - - - - - - + + + - - - - - - + + + + + + @@ -3421,15 +3291,15 @@ - - - + + + @@ -3449,39 +3319,24 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + @@ -3500,46 +3355,40 @@ - - - - - - + + + - - - - - - - - - - + + - - + + + + + + + @@ -3567,18 +3416,15 @@ - - - - - - + + + @@ -3586,9 +3432,6 @@ - - - @@ -3605,20 +3448,17 @@ - - - + + + - - - @@ -3627,9 +3467,6 @@ - - - @@ -3637,6 +3474,25 @@ + + + + + + + + + + + + + + + + + + + @@ -3658,9 +3514,6 @@ - - - @@ -3669,9 +3522,6 @@ - - - @@ -3680,9 +3530,6 @@ - - - @@ -3691,9 +3538,6 @@ - - - @@ -3702,9 +3546,6 @@ - - - @@ -3713,9 +3554,6 @@ - - - @@ -3737,29 +3575,29 @@ - - - + + + - - - - - - + + + + + + @@ -3767,46 +3605,46 @@ - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -3929,40 +3767,26 @@ - - - - - - - - - - - - - - - - + + - - - + + + @@ -3978,15 +3802,15 @@ - - - + + + @@ -3999,18 +3823,15 @@ - - - - - - + + + @@ -4033,18 +3854,15 @@ - - - - - - + + + @@ -4052,40 +3870,37 @@ - - - + + + - - - + + + - - - - - - + + + @@ -4112,15 +3927,15 @@ - - - + + + @@ -4317,9 +4132,6 @@ - - - @@ -4327,26 +4139,21 @@ - - - - - - - - + + + @@ -4354,15 +4161,15 @@ - - - + + + @@ -4370,15 +4177,15 @@ - - - + + + @@ -4386,15 +4193,15 @@ - - - + + + @@ -4452,9 +4259,6 @@ - - - @@ -4476,9 +4280,6 @@ - - - @@ -4495,9 +4296,6 @@ - - - @@ -4514,9 +4312,6 @@ - - - @@ -4533,9 +4328,6 @@ - - - @@ -4552,9 +4344,6 @@ - - - @@ -4563,9 +4352,6 @@ - - - @@ -4582,9 +4368,6 @@ - - - @@ -4593,9 +4376,6 @@ - - - @@ -4627,26 +4407,26 @@ - - - + + + - - - + + + @@ -4662,15 +4442,15 @@ - - - + + + @@ -4678,31 +4458,28 @@ - - - + + + - - - + + + - - - @@ -4719,9 +4496,6 @@ - - - @@ -4738,9 +4512,6 @@ - - - @@ -4757,9 +4528,6 @@ - - - @@ -4776,9 +4544,6 @@ - - - @@ -4795,9 +4560,6 @@ - - - @@ -4814,9 +4576,6 @@ - - - @@ -4833,9 +4592,6 @@ - - - @@ -4862,9 +4618,6 @@ - - - @@ -4881,9 +4634,6 @@ - - - @@ -4900,9 +4650,6 @@ - - - @@ -4919,9 +4666,6 @@ - - - @@ -4930,9 +4674,6 @@ - - - @@ -4949,9 +4690,6 @@ - - - @@ -4960,46 +4698,40 @@ - - - - - - + + + - - - - - - - - - - + + - - + + + + + + + @@ -5007,74 +4739,74 @@ - - - - - - - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + + + + + + + @@ -5087,32 +4819,26 @@ - - - - - - + + + - - - - - - + + + @@ -5123,29 +4849,26 @@ - - - + + + - - - - - - + + + @@ -5178,26 +4901,26 @@ - - - + + + - - - + + + @@ -5220,23 +4943,17 @@ - - - + + + - - - - - - @@ -5270,99 +4987,84 @@ - - - - - - + + + - - - - - - + + + - - - + + + - - - - - - + + + - - - - - - + + + - - - - - - - - - - + + - - + + + + + + + @@ -5372,13 +5074,21 @@ - - - + + + + + + - - + + + + + + + @@ -5400,23 +5110,17 @@ - - - - - - + + + - - - @@ -5433,9 +5137,6 @@ - - - @@ -5464,18 +5165,18 @@ - - - - - - + + + + + + @@ -5511,73 +5212,95 @@ - - - - - - + + + - - - - - - + + + - - - + + + - - + + + + + + + + + + + + + + + + - - - + + + - - + + - - + + - - + + + + + + + + - - + + + + - - + + + + + + + + @@ -5586,9 +5309,6 @@ - - - @@ -5597,9 +5317,6 @@ - - - @@ -5613,9 +5330,6 @@ - - - @@ -5624,9 +5338,6 @@ - - - @@ -5643,88 +5354,70 @@ - - - + + + - - - - - - - - - - + + - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - @@ -5765,9 +5458,6 @@ - - - @@ -5807,11 +5497,6 @@ - - - - - @@ -5821,28 +5506,28 @@ - - - + + + - - - + + + - - + + @@ -5854,17 +5539,17 @@ - - - + + + - - + + @@ -5873,31 +5558,28 @@ - - - + + + - - - + + + - - - @@ -5906,9 +5588,6 @@ - - - @@ -5917,9 +5596,6 @@ - - - @@ -5944,9 +5620,6 @@ - - - @@ -5955,15 +5628,15 @@ - - - + + + @@ -5974,15 +5647,15 @@ - - - + + + @@ -5993,9 +5666,6 @@ - - - @@ -6004,65 +5674,65 @@ - - - + + + - - + + - - - + + + - - - + + + - - + + - - - + + + - - - + + + @@ -6073,15 +5743,15 @@ - - - + + + @@ -6092,15 +5762,15 @@ - - - + + + @@ -6119,9 +5789,6 @@ - - - @@ -6130,32 +5797,29 @@ - - - - - - - - - + - - + + + + + + + @@ -6174,9 +5838,6 @@ - - - @@ -6209,9 +5870,6 @@ - - - @@ -6220,9 +5878,6 @@ - - - @@ -6231,9 +5886,6 @@ - - - @@ -6242,26 +5894,26 @@ - - - + + + - - - + + + @@ -6300,23 +5952,20 @@ - - - - - - - - - + + + + + + @@ -6325,51 +5974,39 @@ - - - - - - - - - - - - + + + - - - - - - - - - + + + - - - + + + + + + @@ -6378,26 +6015,17 @@ - - - - - - - - - - - - + + + @@ -6406,48 +6034,48 @@ - - - + + + - - - + + + - - - + + + - - - + + + @@ -6455,17 +6083,11 @@ - - - - - - @@ -6474,15 +6096,15 @@ - - - + + + @@ -6518,9 +6140,6 @@ - - - @@ -6536,28 +6155,22 @@ - - - - - - + + + - - - @@ -6584,16 +6197,21 @@ - - - + + + + + + + + @@ -6601,15 +6219,15 @@ - - - + + + @@ -6635,26 +6253,26 @@ - - - + + + - - - + + + @@ -6662,97 +6280,99 @@ - - - + + + - - - + + + - - - + + + - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + - - - - - - + + + + + + @@ -6765,29 +6385,29 @@ - - - - - - + + + + + + - - - + + + @@ -6798,15 +6418,15 @@ - - - + + + @@ -6814,54 +6434,54 @@ - - - - - - + + + + + + - - - + + + - - - - - - + + + + + + - - - + + + @@ -6872,15 +6492,15 @@ - - - + + + @@ -6923,18 +6543,18 @@ - - - - - - + + + + + + @@ -6977,18 +6597,18 @@ - - - - - - + + + + + + @@ -6999,15 +6619,15 @@ - - - + + + @@ -7051,28 +6671,33 @@ - - - + + + + + + + + + - - - - - + + + + From 10fa182e385699600557e2fb10449bdcbfdad5fa Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Fri, 3 Apr 2026 15:42:53 +0200 Subject: [PATCH 09/11] Remove old settings implementation --- AndroidManifest.xml | 43 ---- .../ui/AppSettingsScreenTest.kt | 6 +- .../screen/SettingsScreenTest.kt | 12 +- .../ui/SubscriptionSettingsScreenTest.kt | 7 +- .../conversation/ConversationUserFlowTest.kt | 2 +- .../screen/SettingsViewModelTest.kt | 17 +- res/layout/group_mms_setting_dialog.xml | 37 --- res/layout/settings_item_view.xml | 44 ---- res/values/colors.xml | 5 - res/values/dimens.xml | 5 - res/values/styles.xml | 27 --- res/xml/preferences_per_subscription.xml | 62 ----- .../di/settings/SettingsBindsModule.kt | 16 +- src/com/android/messaging/ui/UIIntents.java | 20 -- .../android/messaging/ui/UIIntentsImpl.java | 30 --- .../ApplicationSettingsActivity.java | 223 ----------------- .../ui/appsettings/GroupMmsSettingDialog.java | 92 ------- .../PerSubscriptionSettingsActivity.java | 227 ------------------ .../ui/appsettings/PhoneNumberPreference.java | 106 -------- .../ui/appsettings/SettingsActivity.java | 180 -------------- .../{redesign => }/SettingsActivity.kt | 18 +- .../common/SettingsComponents.kt | 2 +- .../common/SettingsScreenDelegate.kt | 2 +- .../delegate/AppSettingsDelegate.kt | 10 +- .../mapper/AppSettingsUiStateMapper.kt | 4 +- .../model/AppSettingsUiState.kt | 2 +- .../ui/AppSettingsScreen.kt | 12 +- .../screen/SettingsEffectHandler.kt | 22 +- .../screen/SettingsMainScreen.kt | 6 +- .../{redesign => }/screen/SettingsScreen.kt | 8 +- .../screen/SettingsViewModel.kt | 41 ++-- .../screen/model/SettingsNavRoute.kt | 2 +- .../screen/model/SettingsScreenEffect.kt | 2 +- .../screen/model/SettingsUiState.kt | 6 +- .../delegate/SubscriptionSettingsDelegate.kt | 10 +- .../SubscriptionSettingsUiStateMapper.kt | 10 +- .../model/SubscriptionSettingsUiState.kt | 2 +- .../ui/SubscriptionSettingsScreen.kt | 15 +- 38 files changed, 120 insertions(+), 1215 deletions(-) rename app/src/androidTest/java/com/android/messaging/ui/appsettings/{redesign/appsettings => general}/ui/AppSettingsScreenTest.kt (96%) rename app/src/androidTest/java/com/android/messaging/ui/appsettings/{redesign => }/screen/SettingsScreenTest.kt (92%) rename app/src/androidTest/java/com/android/messaging/ui/appsettings/{redesign => }/subscription/ui/SubscriptionSettingsScreenTest.kt (97%) rename app/src/androidTest/java/com/android/messaging/ui/{appsettings => }/conversation/ConversationUserFlowTest.kt (97%) rename app/src/test/java/com/android/messaging/ui/appsettings/{redesign => }/screen/SettingsViewModelTest.kt (94%) delete mode 100644 res/layout/group_mms_setting_dialog.xml delete mode 100644 res/layout/settings_item_view.xml delete mode 100644 res/xml/preferences_per_subscription.xml delete mode 100644 src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java delete mode 100644 src/com/android/messaging/ui/appsettings/GroupMmsSettingDialog.java delete mode 100644 src/com/android/messaging/ui/appsettings/PerSubscriptionSettingsActivity.java delete mode 100644 src/com/android/messaging/ui/appsettings/PhoneNumberPreference.java delete mode 100644 src/com/android/messaging/ui/appsettings/SettingsActivity.java rename src/com/android/messaging/ui/appsettings/{redesign => }/SettingsActivity.kt (68%) rename src/com/android/messaging/ui/appsettings/{redesign => }/common/SettingsComponents.kt (99%) rename src/com/android/messaging/ui/appsettings/{redesign => }/common/SettingsScreenDelegate.kt (77%) rename src/com/android/messaging/ui/appsettings/{redesign/appsettings => general}/delegate/AppSettingsDelegate.kt (86%) rename src/com/android/messaging/ui/appsettings/{redesign/appsettings => general}/mapper/AppSettingsUiStateMapper.kt (91%) rename src/com/android/messaging/ui/appsettings/{redesign/appsettings => general}/model/AppSettingsUiState.kt (82%) rename src/com/android/messaging/ui/appsettings/{redesign/appsettings => general}/ui/AppSettingsScreen.kt (91%) rename src/com/android/messaging/ui/appsettings/{redesign => }/screen/SettingsEffectHandler.kt (70%) rename src/com/android/messaging/ui/appsettings/{redesign => }/screen/SettingsMainScreen.kt (91%) rename src/com/android/messaging/ui/appsettings/{redesign => }/screen/SettingsScreen.kt (94%) rename src/com/android/messaging/ui/appsettings/{redesign => }/screen/SettingsViewModel.kt (76%) rename src/com/android/messaging/ui/appsettings/{redesign => }/screen/model/SettingsNavRoute.kt (86%) rename src/com/android/messaging/ui/appsettings/{redesign => }/screen/model/SettingsScreenEffect.kt (84%) rename src/com/android/messaging/ui/appsettings/{redesign => }/screen/model/SettingsUiState.kt (51%) rename src/com/android/messaging/ui/appsettings/{redesign => }/subscription/delegate/SubscriptionSettingsDelegate.kt (91%) rename src/com/android/messaging/ui/appsettings/{redesign => }/subscription/mapper/SubscriptionSettingsUiStateMapper.kt (94%) rename src/com/android/messaging/ui/appsettings/{redesign => }/subscription/model/SubscriptionSettingsUiState.kt (89%) rename src/com/android/messaging/ui/appsettings/{redesign => }/subscription/ui/SubscriptionSettingsScreen.kt (95%) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a55a1b2d..c41bbdfe 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -164,49 +164,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/settings_item_view.xml b/res/layout/settings_item_view.xml deleted file mode 100644 index a434c111..00000000 --- a/res/layout/settings_item_view.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - diff --git a/res/values/colors.xml b/res/values/colors.xml index 19ee2ec8..fe9c0723 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -151,11 +151,6 @@ #03a9f4 #4d4d4d - #4d4d4d - #6d6d6d - #333333 - #000000 - @color/primary_color #424242 diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 59a4148f..a69dd85d 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -143,11 +143,6 @@ 10dp 5dp - 16sp - 14sp - 60dp - 14sp - 40dp 30dp diff --git a/res/values/styles.xml b/res/values/styles.xml index fa5884d4..cc9ff474 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -67,10 +67,6 @@ @color/archived_conversation_action_bar_background_color_dark - - - - - - - -