diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1a28947f3..7bd516873 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -91,7 +91,7 @@ diff --git a/app/src/androidTest/kotlin/com/android/common/test/helpers/SmsWarningHelper.kt b/app/src/androidTest/kotlin/com/android/common/test/helpers/SmsWarningHelper.kt new file mode 100644 index 000000000..572b3567d --- /dev/null +++ b/app/src/androidTest/kotlin/com/android/common/test/helpers/SmsWarningHelper.kt @@ -0,0 +1,28 @@ +package com.android.common.test.helpers + +import com.android.messaging.BuildConfig +import com.android.messaging.util.BuglePrefs +import com.android.messaging.util.BuglePrefsKeys + +object SmsWarningHelper { + + fun acknowledgeSmsWarning(): Int { + val previousAcknowledgedVersion = BuglePrefs.getApplicationPrefs().getInt( + BuglePrefsKeys.SMS_WARNING_ACKNOWLEDGED_VERSION, + BuglePrefsKeys.SMS_WARNING_ACKNOWLEDGED_VERSION_DEFAULT, + ) + BuglePrefs.getApplicationPrefs().putInt( + BuglePrefsKeys.SMS_WARNING_ACKNOWLEDGED_VERSION, + BuildConfig.VERSION_CODE, + ) + + return previousAcknowledgedVersion + } + + fun restoreSmsWarning(previousAcknowledgedVersion: Int) { + BuglePrefs.getApplicationPrefs().putInt( + BuglePrefsKeys.SMS_WARNING_ACKNOWLEDGED_VERSION, + previousAcknowledgedVersion, + ) + } +} diff --git a/app/src/androidTest/kotlin/com/android/common/test/rules/AppTestRule.kt b/app/src/androidTest/kotlin/com/android/common/test/rules/AppTestRule.kt index c52832f93..843a0c744 100644 --- a/app/src/androidTest/kotlin/com/android/common/test/rules/AppTestRule.kt +++ b/app/src/androidTest/kotlin/com/android/common/test/rules/AppTestRule.kt @@ -1,6 +1,7 @@ package com.android.common.test.rules import com.android.common.test.helpers.ShellCommandHelper +import com.android.common.test.helpers.SmsWarningHelper import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -14,6 +15,7 @@ class AppTestRule : TestRule { return object : Statement() { override fun evaluate() { val previousSmsRoleHolders = ShellCommandHelper.setupSmsDefaultRole() + val previousAcknowledgedVersion = SmsWarningHelper.acknowledgeSmsWarning() var baseFailure: Throwable? = null try { base.evaluate() @@ -21,6 +23,9 @@ class AppTestRule : TestRule { baseFailure = throwable } finally { try { + SmsWarningHelper.restoreSmsWarning( + previousAcknowledgedVersion = previousAcknowledgedVersion, + ) ShellCommandHelper.restoreSmsDefaultRole( previousRoleHolders = previousSmsRoleHolders, ) diff --git a/app/src/test/java/com/android/messaging/ui/permissioncheck/screen/PermissionCheckViewModelTest.kt b/app/src/test/java/com/android/messaging/ui/onboarding/screen/OnboardingViewModelTest.kt similarity index 67% rename from app/src/test/java/com/android/messaging/ui/permissioncheck/screen/PermissionCheckViewModelTest.kt rename to app/src/test/java/com/android/messaging/ui/onboarding/screen/OnboardingViewModelTest.kt index 5f5179b29..9d06dc6dc 100644 --- a/app/src/test/java/com/android/messaging/ui/permissioncheck/screen/PermissionCheckViewModelTest.kt +++ b/app/src/test/java/com/android/messaging/ui/onboarding/screen/OnboardingViewModelTest.kt @@ -1,17 +1,20 @@ -package com.android.messaging.ui.permissioncheck.screen +package com.android.messaging.ui.onboarding.screen import app.cash.turbine.test -import com.android.messaging.data.permissioncheck.GetMissingPermissionLabels -import com.android.messaging.data.permissioncheck.RequiredPermissionsChecker -import com.android.messaging.domain.permissioncheck.model.PermissionRequest -import com.android.messaging.domain.permissioncheck.usecase.DeterminePermissionRequest +import com.android.messaging.data.onboarding.GetMissingPermissionLabels +import com.android.messaging.data.onboarding.RequiredPermissionsChecker +import com.android.messaging.data.onboarding.store.SmsWarningStore +import com.android.messaging.domain.onboarding.model.PermissionRequest +import com.android.messaging.domain.onboarding.usecase.DeterminePermissionRequest import com.android.messaging.testutil.MainDispatcherRule -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckAction as Action -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckScreenEffect as Effect -import com.android.messaging.ui.permissioncheck.screen.model.SettingsGuidance +import com.android.messaging.ui.onboarding.screen.model.OnboardingAction as Action +import com.android.messaging.ui.onboarding.screen.model.OnboardingScreenEffect as Effect +import com.android.messaging.ui.onboarding.screen.model.OnboardingUiState as State +import com.android.messaging.ui.onboarding.screen.model.SettingsGuidance import com.android.messaging.util.core.ElapsedRealtimeProvider import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -21,11 +24,93 @@ import org.junit.Rule import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class PermissionCheckViewModelTest { +class OnboardingViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() + @Test + fun initialState_whenWarningNotAcknowledged_isSmsWarning() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel( + smsWarningStore = mockSmsWarningStore(isAcknowledged = false), + ) + + assertEquals(State.SmsWarning, viewModel.uiState.value) + } + } + + @Test + fun initialState_whenWarningAcknowledged_isPermissions() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel( + smsWarningStore = mockSmsWarningStore(isAcknowledged = true), + ) + + assertEquals(State.Permissions(), viewModel.uiState.value) + } + } + + @Test + fun onScreenResumed_onSmsWarningStep_whenPermissionsGranted_doesNotRedirect() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel( + checker = mockChecker(hasRequiredPermissions = true), + smsWarningStore = mockSmsWarningStore(isAcknowledged = false), + ) + + viewModel.effects.test { + viewModel.onScreenResumed() + + expectNoEvents() + cancelAndIgnoreRemainingEvents() + } + + assertEquals(State.SmsWarning, viewModel.uiState.value) + } + } + + @Test + fun nextClicked_onSmsWarningStep_whenPermissionsMissing_acknowledgesAndShowsPermissions() { + runTest(context = mainDispatcherRule.testDispatcher) { + val smsWarningStore = mockSmsWarningStore(isAcknowledged = false) + val viewModel = createViewModel( + checker = mockChecker(hasRequiredPermissions = false), + smsWarningStore = smsWarningStore, + ) + + viewModel.effects.test { + viewModel.onAction(Action.NextClicked) + + expectNoEvents() + cancelAndIgnoreRemainingEvents() + } + + verify { smsWarningStore.acknowledge() } + assertEquals(State.Permissions(), viewModel.uiState.value) + } + } + + @Test + fun nextClicked_onSmsWarningStep_whenPermissionsGranted_acknowledgesAndRedirects() { + runTest(context = mainDispatcherRule.testDispatcher) { + val smsWarningStore = mockSmsWarningStore(isAcknowledged = false) + val viewModel = createViewModel( + checker = mockChecker(hasRequiredPermissions = true), + smsWarningStore = smsWarningStore, + ) + + viewModel.effects.test { + viewModel.onAction(Action.NextClicked) + + assertEquals(Effect.Redirect, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + + verify { smsWarningStore.acknowledge() } + } + } + @Test fun onScreenResumed_whenPermissionsGranted_emitsRedirect() { runTest(context = mainDispatcherRule.testDispatcher) { @@ -131,7 +216,7 @@ class PermissionCheckViewModelTest { cancelAndIgnoreRemainingEvents() } - assertNull(viewModel.uiState.value.settingsGuidance) + assertNull(viewModel.permissionsState().settingsGuidance) } } @@ -158,8 +243,9 @@ class PermissionCheckViewModelTest { viewModel.onRequestResult() - assertEquals(SettingsGuidance.DefaultSmsApp, viewModel.uiState.value.settingsGuidance) - assertEquals(persistentListOf(), viewModel.uiState.value.missingPermissions) + val state = viewModel.permissionsState() + assertEquals(SettingsGuidance.DefaultSmsApp, state.settingsGuidance) + assertEquals(persistentListOf(), state.missingPermissions) } } @@ -190,8 +276,9 @@ class PermissionCheckViewModelTest { viewModel.onRequestResult() - assertEquals(SettingsGuidance.Permissions, viewModel.uiState.value.settingsGuidance) - assertEquals(labels, viewModel.uiState.value.missingPermissions) + val state = viewModel.permissionsState() + assertEquals(SettingsGuidance.Permissions, state.settingsGuidance) + assertEquals(labels, state.missingPermissions) } } @@ -217,7 +304,7 @@ class PermissionCheckViewModelTest { viewModel.onRequestResult() - assertNull(viewModel.uiState.value.settingsGuidance) + assertNull(viewModel.permissionsState().settingsGuidance) } } @@ -242,7 +329,7 @@ class PermissionCheckViewModelTest { viewModel.onRequestResult() - assertNull(viewModel.uiState.value.settingsGuidance) + assertNull(viewModel.permissionsState().settingsGuidance) } } @@ -255,15 +342,27 @@ class PermissionCheckViewModelTest { persistentListOf() }, elapsedRealtimeProvider: ElapsedRealtimeProvider = ElapsedRealtimeProvider { 0L }, - ): PermissionCheckViewModel { - return PermissionCheckViewModel( + smsWarningStore: SmsWarningStore = mockSmsWarningStore(isAcknowledged = true), + ): OnboardingViewModel { + return OnboardingViewModel( checker = checker, determinePermissionRequest = determinePermissionRequest, getMissingPermissionLabels = getMissingPermissionLabels, elapsedRealtimeProvider = elapsedRealtimeProvider, + smsWarningStore = smsWarningStore, ) } + private fun OnboardingViewModel.permissionsState(): State.Permissions { + return uiState.value as State.Permissions + } + + private fun mockSmsWarningStore(isAcknowledged: Boolean): SmsWarningStore { + return mockk(relaxed = true).also { + every { it.isAcknowledged() } returns isAcknowledged + } + } + private fun mockChecker( hasRequiredPermissions: Boolean, isSmsRoleHeld: Boolean = false, diff --git a/app/src/test/kotlin/com/android/messaging/data/permissioncheck/GetMissingPermissionLabelsImplTest.kt b/app/src/test/kotlin/com/android/messaging/data/onboarding/GetMissingPermissionLabelsImplTest.kt similarity index 97% rename from app/src/test/kotlin/com/android/messaging/data/permissioncheck/GetMissingPermissionLabelsImplTest.kt rename to app/src/test/kotlin/com/android/messaging/data/onboarding/GetMissingPermissionLabelsImplTest.kt index cd5f55ad3..5833a86fc 100644 --- a/app/src/test/kotlin/com/android/messaging/data/permissioncheck/GetMissingPermissionLabelsImplTest.kt +++ b/app/src/test/kotlin/com/android/messaging/data/onboarding/GetMissingPermissionLabelsImplTest.kt @@ -1,4 +1,4 @@ -package com.android.messaging.data.permissioncheck +package com.android.messaging.data.onboarding import android.content.pm.PackageManager import android.content.pm.PermissionInfo diff --git a/app/src/test/kotlin/com/android/messaging/data/onboarding/store/SmsWarningStoreTest.kt b/app/src/test/kotlin/com/android/messaging/data/onboarding/store/SmsWarningStoreTest.kt new file mode 100644 index 000000000..6b227ffb1 --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/data/onboarding/store/SmsWarningStoreTest.kt @@ -0,0 +1,50 @@ +package com.android.messaging.data.onboarding.store + +import com.android.messaging.FactoryTestAccess +import com.android.messaging.testutil.FakeBuglePrefs +import com.android.messaging.testutil.installTestFactory +import io.mockk.mockk +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class SmsWarningStoreTest { + + @Before + fun setUp() { + installTestFactory( + context = mockk(relaxed = true), + prefs = FakeBuglePrefs(), + ) + } + + @After + fun tearDown() { + FactoryTestAccess.reset() + } + + @Test + fun isAcknowledged_whenNothingStored_returnsFalse() { + assertFalse(createStore(versionCode = 42).isAcknowledged()) + } + + @Test + fun isAcknowledged_afterAcknowledgeOnSameBuild_returnsTrue() { + createStore(versionCode = 42).acknowledge() + assertTrue(createStore(versionCode = 42).isAcknowledged()) + } + + @Test + fun isAcknowledged_afterAcknowledgeOnEarlierBuild_returnsFalse() { + createStore(versionCode = 42).acknowledge() + assertFalse(createStore(versionCode = 43).isAcknowledged()) + } + + private fun createStore(versionCode: Int): SmsWarningStore { + return SmsWarningStoreImpl( + appVersionProvider = { versionCode }, + ) + } +} diff --git a/app/src/test/kotlin/com/android/messaging/domain/permissioncheck/usecase/DeterminePermissionRequestTest.kt b/app/src/test/kotlin/com/android/messaging/domain/onboarding/usecase/DeterminePermissionRequestTest.kt similarity index 89% rename from app/src/test/kotlin/com/android/messaging/domain/permissioncheck/usecase/DeterminePermissionRequestTest.kt rename to app/src/test/kotlin/com/android/messaging/domain/onboarding/usecase/DeterminePermissionRequestTest.kt index 60a71fe31..83b818b0f 100644 --- a/app/src/test/kotlin/com/android/messaging/domain/permissioncheck/usecase/DeterminePermissionRequestTest.kt +++ b/app/src/test/kotlin/com/android/messaging/domain/onboarding/usecase/DeterminePermissionRequestTest.kt @@ -1,7 +1,7 @@ -package com.android.messaging.domain.permissioncheck.usecase +package com.android.messaging.domain.onboarding.usecase -import com.android.messaging.data.permissioncheck.RequiredPermissionsChecker -import com.android.messaging.domain.permissioncheck.model.PermissionRequest +import com.android.messaging.data.onboarding.RequiredPermissionsChecker +import com.android.messaging.domain.onboarding.model.PermissionRequest import io.mockk.every import io.mockk.mockk import kotlinx.collections.immutable.ImmutableList diff --git a/app/src/test/kotlin/com/android/messaging/domain/onboarding/usecase/ShouldShowOnboardingTest.kt b/app/src/test/kotlin/com/android/messaging/domain/onboarding/usecase/ShouldShowOnboardingTest.kt new file mode 100644 index 000000000..5ab2bd289 --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/domain/onboarding/usecase/ShouldShowOnboardingTest.kt @@ -0,0 +1,69 @@ +package com.android.messaging.domain.onboarding.usecase + +import com.android.messaging.data.onboarding.RequiredPermissionsChecker +import com.android.messaging.data.onboarding.store.SmsWarningStore +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ShouldShowOnboardingTest { + + @Test + fun invoke_whenWarningPendingAndPermissionsGranted_returnsTrue() { + val useCase = createUseCase( + isAcknowledged = false, + hasRequiredPermissions = true, + ) + + assertTrue(useCase()) + } + + @Test + fun invoke_whenWarningAcknowledgedAndPermissionsMissing_returnsTrue() { + val useCase = createUseCase( + isAcknowledged = true, + hasRequiredPermissions = false, + ) + + assertTrue(useCase()) + } + + @Test + fun invoke_whenWarningPendingAndPermissionsMissing_returnsTrue() { + val useCase = createUseCase( + isAcknowledged = false, + hasRequiredPermissions = false, + ) + + assertTrue(useCase()) + } + + @Test + fun invoke_whenWarningAcknowledgedAndPermissionsGranted_returnsFalse() { + val useCase = createUseCase( + isAcknowledged = true, + hasRequiredPermissions = true, + ) + + assertFalse(useCase()) + } + + private fun createUseCase( + isAcknowledged: Boolean, + hasRequiredPermissions: Boolean, + ): ShouldShowOnboarding { + val smsWarningStore = mockk().also { + every { it.isAcknowledged() } returns isAcknowledged + } + val checker = mockk().also { + every { it.hasRequiredPermissions() } returns hasRequiredPermissions + } + + return ShouldShowOnboardingImpl( + checker = checker, + smsWarningStore = smsWarningStore, + ) + } +} diff --git a/app/src/test/kotlin/com/android/messaging/ui/ActivityPermissionGateTest.kt b/app/src/test/kotlin/com/android/messaging/ui/ActivityPermissionGateTest.kt index de4f29761..9e7cf5e4d 100644 --- a/app/src/test/kotlin/com/android/messaging/ui/ActivityPermissionGateTest.kt +++ b/app/src/test/kotlin/com/android/messaging/ui/ActivityPermissionGateTest.kt @@ -4,7 +4,7 @@ import com.android.messaging.ui.classzero.ClassZeroActivity import com.android.messaging.ui.contact.AddContactActivity import com.android.messaging.ui.conversation.LaunchConversationActivity import com.android.messaging.ui.license.LicenseActivity -import com.android.messaging.ui.permissioncheck.PermissionCheckActivity +import com.android.messaging.ui.onboarding.OnboardingActivity import com.android.messaging.ui.photoviewer.PhotoViewerActivity import java.io.File import javax.xml.parsers.DocumentBuilderFactory @@ -24,7 +24,7 @@ internal class ActivityPermissionGateTest { ) private val intentionallyUngated = setOf>( - PermissionCheckActivity::class.java, + OnboardingActivity::class.java, LaunchConversationActivity::class.java, PhotoViewerActivity::class.java, LicenseActivity::class.java, diff --git a/res/values/strings.xml b/res/values/strings.xml index c9c5c2445..2fb29ed9c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -35,6 +35,9 @@ Next Exit Settings + + SMS isn\'t private + Text messages aren\'t encrypted. Use an app with end-to-end encryption for anything sensitive. Messaging needs permission to SMS, Phone and Contacts. diff --git a/src/com/android/messaging/data/permissioncheck/GetMissingPermissionLabels.kt b/src/com/android/messaging/data/onboarding/GetMissingPermissionLabels.kt similarity index 95% rename from src/com/android/messaging/data/permissioncheck/GetMissingPermissionLabels.kt rename to src/com/android/messaging/data/onboarding/GetMissingPermissionLabels.kt index bd317be8b..4ff22b1ea 100644 --- a/src/com/android/messaging/data/permissioncheck/GetMissingPermissionLabels.kt +++ b/src/com/android/messaging/data/onboarding/GetMissingPermissionLabels.kt @@ -1,4 +1,4 @@ -package com.android.messaging.data.permissioncheck +package com.android.messaging.data.onboarding import android.content.pm.PackageManager import javax.inject.Inject diff --git a/src/com/android/messaging/data/permissioncheck/RequiredPermissionsChecker.kt b/src/com/android/messaging/data/onboarding/RequiredPermissionsChecker.kt similarity index 94% rename from src/com/android/messaging/data/permissioncheck/RequiredPermissionsChecker.kt rename to src/com/android/messaging/data/onboarding/RequiredPermissionsChecker.kt index 700a08b99..9de843fef 100644 --- a/src/com/android/messaging/data/permissioncheck/RequiredPermissionsChecker.kt +++ b/src/com/android/messaging/data/onboarding/RequiredPermissionsChecker.kt @@ -1,4 +1,4 @@ -package com.android.messaging.data.permissioncheck +package com.android.messaging.data.onboarding import android.app.role.RoleManager import com.android.messaging.util.OsUtil diff --git a/src/com/android/messaging/data/onboarding/store/SmsWarningStore.kt b/src/com/android/messaging/data/onboarding/store/SmsWarningStore.kt new file mode 100644 index 000000000..28e665a70 --- /dev/null +++ b/src/com/android/messaging/data/onboarding/store/SmsWarningStore.kt @@ -0,0 +1,31 @@ +package com.android.messaging.data.onboarding.store + +import com.android.messaging.util.BuglePrefs +import com.android.messaging.util.BuglePrefsKeys +import com.android.messaging.util.core.AppVersionProvider +import javax.inject.Inject + +internal interface SmsWarningStore { + fun isAcknowledged(): Boolean + fun acknowledge() +} + +internal class SmsWarningStoreImpl @Inject constructor( + private val appVersionProvider: AppVersionProvider, +) : SmsWarningStore { + + override fun isAcknowledged(): Boolean { + val acknowledgedVersion = BuglePrefs.getApplicationPrefs().getInt( + BuglePrefsKeys.SMS_WARNING_ACKNOWLEDGED_VERSION, + BuglePrefsKeys.SMS_WARNING_ACKNOWLEDGED_VERSION_DEFAULT, + ) + return acknowledgedVersion == appVersionProvider.versionCode() + } + + override fun acknowledge() { + BuglePrefs.getApplicationPrefs().putInt( + BuglePrefsKeys.SMS_WARNING_ACKNOWLEDGED_VERSION, + appVersionProvider.versionCode(), + ) + } +} diff --git a/src/com/android/messaging/di/core/CoreProvidesModule.kt b/src/com/android/messaging/di/core/CoreProvidesModule.kt index 4c49744d4..5397a5b92 100644 --- a/src/com/android/messaging/di/core/CoreProvidesModule.kt +++ b/src/com/android/messaging/di/core/CoreProvidesModule.kt @@ -8,6 +8,8 @@ import android.content.pm.PackageManager import android.os.SystemClock import android.telephony.SubscriptionManager import android.telephony.TelephonyManager +import com.android.messaging.BuildConfig +import com.android.messaging.util.core.AppVersionProvider import com.android.messaging.util.core.ElapsedRealtimeProvider import dagger.Module import dagger.Provides @@ -129,4 +131,10 @@ internal class CoreProvidesModule { fun provideElapsedRealtimeProvider(): ElapsedRealtimeProvider { return ElapsedRealtimeProvider { SystemClock.elapsedRealtime() } } + + @Provides + @Reusable + fun provideAppVersionProvider(): AppVersionProvider { + return AppVersionProvider { BuildConfig.VERSION_CODE } + } } diff --git a/src/com/android/messaging/di/onboarding/OnboardingBindsModule.kt b/src/com/android/messaging/di/onboarding/OnboardingBindsModule.kt new file mode 100644 index 000000000..ca7d4b50e --- /dev/null +++ b/src/com/android/messaging/di/onboarding/OnboardingBindsModule.kt @@ -0,0 +1,52 @@ +package com.android.messaging.di.onboarding + +import com.android.messaging.data.onboarding.GetMissingPermissionLabels +import com.android.messaging.data.onboarding.GetMissingPermissionLabelsImpl +import com.android.messaging.data.onboarding.RequiredPermissionsChecker +import com.android.messaging.data.onboarding.RequiredPermissionsCheckerImpl +import com.android.messaging.data.onboarding.store.SmsWarningStore +import com.android.messaging.data.onboarding.store.SmsWarningStoreImpl +import com.android.messaging.domain.onboarding.usecase.DeterminePermissionRequest +import com.android.messaging.domain.onboarding.usecase.DeterminePermissionRequestImpl +import com.android.messaging.domain.onboarding.usecase.ShouldShowOnboarding +import com.android.messaging.domain.onboarding.usecase.ShouldShowOnboardingImpl +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 OnboardingBindsModule { + + @Binds + @Reusable + abstract fun bindRequiredPermissionsChecker( + impl: RequiredPermissionsCheckerImpl, + ): RequiredPermissionsChecker + + @Binds + @Reusable + abstract fun bindDeterminePermissionRequest( + impl: DeterminePermissionRequestImpl, + ): DeterminePermissionRequest + + @Binds + @Reusable + abstract fun bindGetMissingPermissionLabels( + impl: GetMissingPermissionLabelsImpl, + ): GetMissingPermissionLabels + + @Binds + @Reusable + abstract fun bindSmsWarningStore( + impl: SmsWarningStoreImpl, + ): SmsWarningStore + + @Binds + @Reusable + abstract fun bindShouldShowOnboarding( + impl: ShouldShowOnboardingImpl, + ): ShouldShowOnboarding +} diff --git a/src/com/android/messaging/di/permissioncheck/PermissionCheckBindsModule.kt b/src/com/android/messaging/di/permissioncheck/PermissionCheckBindsModule.kt deleted file mode 100644 index 12d6431b0..000000000 --- a/src/com/android/messaging/di/permissioncheck/PermissionCheckBindsModule.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.android.messaging.di.permissioncheck - -import com.android.messaging.data.permissioncheck.GetMissingPermissionLabels -import com.android.messaging.data.permissioncheck.GetMissingPermissionLabelsImpl -import com.android.messaging.data.permissioncheck.RequiredPermissionsChecker -import com.android.messaging.data.permissioncheck.RequiredPermissionsCheckerImpl -import com.android.messaging.domain.permissioncheck.usecase.DeterminePermissionRequest -import com.android.messaging.domain.permissioncheck.usecase.DeterminePermissionRequestImpl -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 PermissionCheckBindsModule { - - @Binds - @Reusable - abstract fun bindRequiredPermissionsChecker( - impl: RequiredPermissionsCheckerImpl, - ): RequiredPermissionsChecker - - @Binds - @Reusable - abstract fun bindDeterminePermissionRequest( - impl: DeterminePermissionRequestImpl, - ): DeterminePermissionRequest - - @Binds - @Reusable - abstract fun bindGetMissingPermissionLabels( - impl: GetMissingPermissionLabelsImpl, - ): GetMissingPermissionLabels -} diff --git a/src/com/android/messaging/domain/permissioncheck/model/PermissionRequest.kt b/src/com/android/messaging/domain/onboarding/model/PermissionRequest.kt similarity index 83% rename from src/com/android/messaging/domain/permissioncheck/model/PermissionRequest.kt rename to src/com/android/messaging/domain/onboarding/model/PermissionRequest.kt index 2f7ebae58..b373a43a2 100644 --- a/src/com/android/messaging/domain/permissioncheck/model/PermissionRequest.kt +++ b/src/com/android/messaging/domain/onboarding/model/PermissionRequest.kt @@ -1,4 +1,4 @@ -package com.android.messaging.domain.permissioncheck.model +package com.android.messaging.domain.onboarding.model import kotlinx.collections.immutable.ImmutableList diff --git a/src/com/android/messaging/domain/permissioncheck/usecase/DeterminePermissionRequest.kt b/src/com/android/messaging/domain/onboarding/usecase/DeterminePermissionRequest.kt similarity index 74% rename from src/com/android/messaging/domain/permissioncheck/usecase/DeterminePermissionRequest.kt rename to src/com/android/messaging/domain/onboarding/usecase/DeterminePermissionRequest.kt index 120d9cdb5..2584428a3 100644 --- a/src/com/android/messaging/domain/permissioncheck/usecase/DeterminePermissionRequest.kt +++ b/src/com/android/messaging/domain/onboarding/usecase/DeterminePermissionRequest.kt @@ -1,7 +1,7 @@ -package com.android.messaging.domain.permissioncheck.usecase +package com.android.messaging.domain.onboarding.usecase -import com.android.messaging.data.permissioncheck.RequiredPermissionsChecker -import com.android.messaging.domain.permissioncheck.model.PermissionRequest +import com.android.messaging.data.onboarding.RequiredPermissionsChecker +import com.android.messaging.domain.onboarding.model.PermissionRequest import javax.inject.Inject internal fun interface DeterminePermissionRequest { diff --git a/src/com/android/messaging/domain/onboarding/usecase/ShouldShowOnboarding.kt b/src/com/android/messaging/domain/onboarding/usecase/ShouldShowOnboarding.kt new file mode 100644 index 000000000..76a1c12a7 --- /dev/null +++ b/src/com/android/messaging/domain/onboarding/usecase/ShouldShowOnboarding.kt @@ -0,0 +1,31 @@ +package com.android.messaging.domain.onboarding.usecase + +import com.android.messaging.data.onboarding.RequiredPermissionsChecker +import com.android.messaging.data.onboarding.store.SmsWarningStore +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Inject + +internal interface ShouldShowOnboarding { + operator fun invoke(): Boolean + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface Provider { + fun shouldShowOnboarding(): ShouldShowOnboarding + } +} + +internal class ShouldShowOnboardingImpl @Inject constructor( + private val checker: RequiredPermissionsChecker, + private val smsWarningStore: SmsWarningStore, +) : ShouldShowOnboarding { + + override fun invoke(): Boolean { + val needsSmsWarning = !smsWarningStore.isAcknowledged() + val needsPermissions = !checker.hasRequiredPermissions() + + return needsSmsWarning || needsPermissions + } +} diff --git a/src/com/android/messaging/ui/BaseBugleActivity.java b/src/com/android/messaging/ui/BaseBugleActivity.java index 43ba1d1de..02a316888 100644 --- a/src/com/android/messaging/ui/BaseBugleActivity.java +++ b/src/com/android/messaging/ui/BaseBugleActivity.java @@ -32,7 +32,7 @@ public class BaseBugleActivity extends AppCompatActivity { @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (UiUtils.redirectToPermissionCheckIfNeeded(this)) { + if (UiUtils.redirectToOnboardingIfNeeded(this)) { return; } } diff --git a/src/com/android/messaging/ui/BugleActionBarActivity.java b/src/com/android/messaging/ui/BugleActionBarActivity.java index b7e72cd72..ea04b9481 100644 --- a/src/com/android/messaging/ui/BugleActionBarActivity.java +++ b/src/com/android/messaging/ui/BugleActionBarActivity.java @@ -61,7 +61,7 @@ public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (UiUtils.redirectToPermissionCheckIfNeeded(this)) { + if (UiUtils.redirectToOnboardingIfNeeded(this)) { return; } diff --git a/src/com/android/messaging/ui/BugleComponentActivity.kt b/src/com/android/messaging/ui/BugleComponentActivity.kt index ff9116b05..f6074d1fc 100644 --- a/src/com/android/messaging/ui/BugleComponentActivity.kt +++ b/src/com/android/messaging/ui/BugleComponentActivity.kt @@ -9,7 +9,7 @@ open class BugleComponentActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - UiUtils.redirectToPermissionCheckIfNeeded(this) + UiUtils.redirectToOnboardingIfNeeded(this) } override fun onResume() { diff --git a/src/com/android/messaging/ui/UIIntents.java b/src/com/android/messaging/ui/UIIntents.java index c8da82758..36ab58e73 100644 --- a/src/com/android/messaging/ui/UIIntents.java +++ b/src/com/android/messaging/ui/UIIntents.java @@ -103,9 +103,9 @@ public static UIIntents get() { public static final String ACTION_MESSAGE_READ = "com.android.messaging.action.MESSAGE_READ"; /** - * Launch the permission check activity + * Launch the onboarding activity (SMS warning, followed by the permission check) */ - public abstract void launchPermissionCheckActivity(final Context context); + public abstract void launchOnboardingActivity(final Context context); public abstract void launchConversationListActivity(final Context context); diff --git a/src/com/android/messaging/ui/UIIntentsImpl.java b/src/com/android/messaging/ui/UIIntentsImpl.java index 438e844c9..719de3d4d 100644 --- a/src/com/android/messaging/ui/UIIntentsImpl.java +++ b/src/com/android/messaging/ui/UIIntentsImpl.java @@ -55,7 +55,7 @@ import com.android.messaging.ui.conversationpicker.host.widget.WidgetPickConversationActivity; import com.android.messaging.ui.conversationsettings.ConversationSettingsActivity; import com.android.messaging.ui.debug.DebugMmsConfigActivity; -import com.android.messaging.ui.permissioncheck.PermissionCheckActivity; +import com.android.messaging.ui.onboarding.OnboardingActivity; import com.android.messaging.ui.photoviewer.PhotoViewerActivity; import com.android.messaging.ui.vcarddetail.VCardDetailActivity; import com.android.messaging.util.Assert; @@ -133,8 +133,8 @@ private Intent getConversationActivityIntent(final Context context, } @Override - public void launchPermissionCheckActivity(final Context context) { - final Intent intent = new Intent(context, PermissionCheckActivity.class); + public void launchOnboardingActivity(final Context context) { + final Intent intent = new Intent(context, OnboardingActivity.class); context.startActivity(intent); } diff --git a/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java b/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java index c869839e0..5c9e10357 100644 --- a/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java +++ b/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java @@ -57,7 +57,7 @@ public class LaunchConversationActivity extends Activity implements @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (UiUtils.redirectToPermissionCheckIfNeeded(this)) { + if (UiUtils.redirectToOnboardingIfNeeded(this)) { return; } diff --git a/src/com/android/messaging/ui/permissioncheck/PermissionCheckActivity.kt b/src/com/android/messaging/ui/onboarding/OnboardingActivity.kt similarity index 68% rename from src/com/android/messaging/ui/permissioncheck/PermissionCheckActivity.kt rename to src/com/android/messaging/ui/onboarding/OnboardingActivity.kt index ada390456..5b5f47ab1 100644 --- a/src/com/android/messaging/ui/permissioncheck/PermissionCheckActivity.kt +++ b/src/com/android/messaging/ui/onboarding/OnboardingActivity.kt @@ -1,4 +1,4 @@ -package com.android.messaging.ui.permissioncheck +package com.android.messaging.ui.onboarding import android.app.role.RoleManager import android.os.Bundle @@ -6,13 +6,13 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.android.messaging.ui.core.AppTheme -import com.android.messaging.ui.permissioncheck.screen.PermissionCheckEffectHandlerImpl -import com.android.messaging.ui.permissioncheck.screen.PermissionCheckScreen +import com.android.messaging.ui.onboarding.screen.OnboardingEffectHandlerImpl +import com.android.messaging.ui.onboarding.screen.OnboardingScreen import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class PermissionCheckActivity : ComponentActivity() { +class OnboardingActivity : ComponentActivity() { @Inject lateinit var roleManager: RoleManager @@ -22,14 +22,14 @@ class PermissionCheckActivity : ComponentActivity() { enableEdgeToEdge() - val effectHandler = PermissionCheckEffectHandlerImpl( + val effectHandler = OnboardingEffectHandlerImpl( activity = this, roleManager = roleManager, ) setContent { AppTheme { - PermissionCheckScreen( + OnboardingScreen( effectHandler = effectHandler, onNavigateBack = ::finish, ) diff --git a/src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckEffectHandler.kt b/src/com/android/messaging/ui/onboarding/screen/OnboardingEffectHandler.kt similarity index 81% rename from src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckEffectHandler.kt rename to src/com/android/messaging/ui/onboarding/screen/OnboardingEffectHandler.kt index 8c845c25a..732510a6f 100644 --- a/src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckEffectHandler.kt +++ b/src/com/android/messaging/ui/onboarding/screen/OnboardingEffectHandler.kt @@ -1,4 +1,4 @@ -package com.android.messaging.ui.permissioncheck.screen +package com.android.messaging.ui.onboarding.screen import android.app.Activity import android.app.role.RoleManager @@ -7,18 +7,18 @@ import android.net.Uri import android.provider.Settings import com.android.messaging.Factory import com.android.messaging.ui.UIIntents -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckScreenEffect as Effect +import com.android.messaging.ui.onboarding.screen.model.OnboardingScreenEffect as Effect -internal interface PermissionCheckEffectHandler { +internal interface OnboardingEffectHandler { fun handle(effect: Effect) fun createSmsRoleIntent(): Intent } -internal class PermissionCheckEffectHandlerImpl( +internal class OnboardingEffectHandlerImpl( private val activity: Activity, private val roleManager: RoleManager, -) : PermissionCheckEffectHandler { +) : OnboardingEffectHandler { override fun handle(effect: Effect) { when (effect) { diff --git a/src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckScreen.kt b/src/com/android/messaging/ui/onboarding/screen/OnboardingScreen.kt similarity index 71% rename from src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckScreen.kt rename to src/com/android/messaging/ui/onboarding/screen/OnboardingScreen.kt index 585f971da..81ad0943a 100644 --- a/src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckScreen.kt +++ b/src/com/android/messaging/ui/onboarding/screen/OnboardingScreen.kt @@ -1,4 +1,4 @@ -package com.android.messaging.ui.permissioncheck.screen +package com.android.messaging.ui.onboarding.screen import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -15,7 +15,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.Button +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -42,22 +45,23 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.android.messaging.R import com.android.messaging.ui.core.MessagingPreviewTheme -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckAction as Action -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckScreenEffect as Effect -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckUiState as State -import com.android.messaging.ui.permissioncheck.screen.model.SettingsGuidance +import com.android.messaging.ui.onboarding.screen.model.OnboardingAction as Action +import com.android.messaging.ui.onboarding.screen.model.OnboardingScreenEffect as Effect +import com.android.messaging.ui.onboarding.screen.model.OnboardingUiState as State +import com.android.messaging.ui.onboarding.screen.model.SettingsGuidance import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf private val HeroIconSize = 96.dp +private val HeroIconPadding = 24.dp private val ScreenPadding = 24.dp @Composable -internal fun PermissionCheckScreen( - effectHandler: PermissionCheckEffectHandler, +internal fun OnboardingScreen( + effectHandler: OnboardingEffectHandler, onNavigateBack: () -> Unit, modifier: Modifier = Modifier, - screenModel: PermissionCheckScreenModel = viewModel(), + screenModel: OnboardingScreenModel = viewModel(), ) { val uiState by screenModel.uiState.collectAsStateWithLifecycle() @@ -95,7 +99,7 @@ internal fun PermissionCheckScreen( screenModel.onScreenResumed() } - PermissionCheckContent( + OnboardingContent( uiState = uiState, onAction = screenModel::onAction, onNavigateBack = onNavigateBack, @@ -104,7 +108,7 @@ internal fun PermissionCheckScreen( } @Composable -private fun PermissionCheckContent( +private fun OnboardingContent( uiState: State, onAction: (Action) -> Unit, onNavigateBack: () -> Unit, @@ -129,31 +133,16 @@ private fun PermissionCheckContent( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - PermissionHero() - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = stringResource(R.string.required_permissions_promo), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - ) - - uiState.settingsGuidance?.let { guidance -> - Spacer(modifier = Modifier.height(24.dp)) - - PermissionSettingsGuidance( - guidance = guidance, - missingPermissions = uiState.missingPermissions, - ) + when (uiState) { + is State.SmsWarning -> SmsWarningStep() + is State.Permissions -> PermissionsStep(uiState = uiState) } } Spacer(modifier = Modifier.height(16.dp)) - PermissionCheckActions( - showSettingsGuidance = uiState.settingsGuidance != null, + OnboardingActions( + uiState = uiState, onAction = onAction, onNavigateBack = onNavigateBack, ) @@ -162,7 +151,34 @@ private fun PermissionCheckContent( } @Composable -private fun PermissionHero() { +private fun SmsWarningStep() { + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = null, + modifier = Modifier + .size(HeroIconSize) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.errorContainer) + .padding(HeroIconPadding), + tint = MaterialTheme.colorScheme.onErrorContainer, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + OnboardingStepTitle(text = stringResource(R.string.sms_warning_title)) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.sms_warning_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun PermissionsStep(uiState: State.Permissions) { Image( painter = painterResource(R.drawable.ic_launcher_foreground), contentDescription = null, @@ -171,6 +187,29 @@ private fun PermissionHero() { .clip(CircleShape) .background(colorResource(R.color.ic_launcher_background)), ) + + Spacer(modifier = Modifier.height(24.dp)) + + OnboardingStepTitle(text = stringResource(R.string.required_permissions_promo)) + + uiState.settingsGuidance?.let { guidance -> + Spacer(modifier = Modifier.height(24.dp)) + + PermissionSettingsGuidance( + guidance = guidance, + missingPermissions = uiState.missingPermissions, + ) + } +} + +@Composable +private fun OnboardingStepTitle(text: String) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) } @Composable @@ -226,16 +265,18 @@ private fun PermissionSettingsGuidance( } @Composable -private fun PermissionCheckActions( - showSettingsGuidance: Boolean, +private fun OnboardingActions( + uiState: State, onAction: (Action) -> Unit, onNavigateBack: () -> Unit, ) { + val showSettingsGuidance = uiState is State.Permissions && uiState.settingsGuidance != null + val primaryActionLabel = when { showSettingsGuidance -> R.string.settings else -> R.string.next } - val onPrimaryActionClick = when { + val primaryAction = when { showSettingsGuidance -> Action.SettingsClicked else -> Action.NextClicked } @@ -245,7 +286,7 @@ private fun PermissionCheckActions( horizontalAlignment = Alignment.CenterHorizontally, ) { Button( - onClick = { onAction(onPrimaryActionClick) }, + onClick = { onAction(primaryAction) }, modifier = Modifier.fillMaxWidth(), ) { Text(text = stringResource(primaryActionLabel)) @@ -264,10 +305,22 @@ private fun PermissionCheckActions( @PreviewLightDark @Composable -private fun PermissionCheckContentPreview() { +private fun OnboardingContentSmsWarningPreview() { + MessagingPreviewTheme { + OnboardingContent( + uiState = State.SmsWarning, + onAction = {}, + onNavigateBack = {}, + ) + } +} + +@PreviewLightDark +@Composable +private fun OnboardingContentPreview() { MessagingPreviewTheme { - PermissionCheckContent( - uiState = State(settingsGuidance = null), + OnboardingContent( + uiState = State.Permissions(), onAction = {}, onNavigateBack = {}, ) @@ -276,10 +329,10 @@ private fun PermissionCheckContentPreview() { @PreviewLightDark @Composable -private fun PermissionCheckContentDefaultSmsAppGuidancePreview() { +private fun OnboardingContentDefaultSmsAppGuidancePreview() { MessagingPreviewTheme { - PermissionCheckContent( - uiState = State(settingsGuidance = SettingsGuidance.DefaultSmsApp), + OnboardingContent( + uiState = State.Permissions(settingsGuidance = SettingsGuidance.DefaultSmsApp), onAction = {}, onNavigateBack = {}, ) @@ -288,10 +341,10 @@ private fun PermissionCheckContentDefaultSmsAppGuidancePreview() { @PreviewLightDark @Composable -private fun PermissionCheckContentPermissionsGuidancePreview() { +private fun OnboardingContentPermissionsGuidancePreview() { MessagingPreviewTheme { - PermissionCheckContent( - uiState = State( + OnboardingContent( + uiState = State.Permissions( settingsGuidance = SettingsGuidance.Permissions, missingPermissions = persistentListOf( "read your text messages", diff --git a/src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckViewModel.kt b/src/com/android/messaging/ui/onboarding/screen/OnboardingViewModel.kt similarity index 63% rename from src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckViewModel.kt rename to src/com/android/messaging/ui/onboarding/screen/OnboardingViewModel.kt index 7e9df3450..3b79267ab 100644 --- a/src/com/android/messaging/ui/permissioncheck/screen/PermissionCheckViewModel.kt +++ b/src/com/android/messaging/ui/onboarding/screen/OnboardingViewModel.kt @@ -1,14 +1,15 @@ -package com.android.messaging.ui.permissioncheck.screen +package com.android.messaging.ui.onboarding.screen import androidx.lifecycle.ViewModel -import com.android.messaging.data.permissioncheck.GetMissingPermissionLabels -import com.android.messaging.data.permissioncheck.RequiredPermissionsChecker -import com.android.messaging.domain.permissioncheck.model.PermissionRequest -import com.android.messaging.domain.permissioncheck.usecase.DeterminePermissionRequest -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckAction as Action -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckScreenEffect as Effect -import com.android.messaging.ui.permissioncheck.screen.model.PermissionCheckUiState as State -import com.android.messaging.ui.permissioncheck.screen.model.SettingsGuidance +import com.android.messaging.data.onboarding.GetMissingPermissionLabels +import com.android.messaging.data.onboarding.RequiredPermissionsChecker +import com.android.messaging.data.onboarding.store.SmsWarningStore +import com.android.messaging.domain.onboarding.model.PermissionRequest +import com.android.messaging.domain.onboarding.usecase.DeterminePermissionRequest +import com.android.messaging.ui.onboarding.screen.model.OnboardingAction as Action +import com.android.messaging.ui.onboarding.screen.model.OnboardingScreenEffect as Effect +import com.android.messaging.ui.onboarding.screen.model.OnboardingUiState as State +import com.android.messaging.ui.onboarding.screen.model.SettingsGuidance import com.android.messaging.util.core.ElapsedRealtimeProvider import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -19,9 +20,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -internal interface PermissionCheckScreenModel { +internal interface OnboardingScreenModel { val effects: Flow val uiState: StateFlow @@ -31,18 +31,24 @@ internal interface PermissionCheckScreenModel { } @HiltViewModel -internal class PermissionCheckViewModel @Inject constructor( +internal class OnboardingViewModel @Inject constructor( private val checker: RequiredPermissionsChecker, private val determinePermissionRequest: DeterminePermissionRequest, private val getMissingPermissionLabels: GetMissingPermissionLabels, private val elapsedRealtimeProvider: ElapsedRealtimeProvider, + private val smsWarningStore: SmsWarningStore, ) : ViewModel(), - PermissionCheckScreenModel { + OnboardingScreenModel { private val _effects = MutableSharedFlow(extraBufferCapacity = 1) override val effects: Flow = _effects.asSharedFlow() - private val _uiState = MutableStateFlow(State()) + private val _uiState = MutableStateFlow( + when { + smsWarningStore.isAcknowledged() -> State.Permissions() + else -> State.SmsWarning + }, + ) override val uiState: StateFlow = _uiState.asStateFlow() private var requestStartedAtMillis = 0L @@ -60,6 +66,10 @@ internal class PermissionCheckViewModel @Inject constructor( } override fun onScreenResumed() { + if (_uiState.value !is State.Permissions) { + return + } + if (checker.hasRequiredPermissions()) { emitEffect(Effect.Redirect) } @@ -82,16 +92,32 @@ internal class PermissionCheckViewModel @Inject constructor( SettingsGuidance.Permissions -> getMissingPermissionLabels() } - _uiState.update { - it.copy( - settingsGuidance = guidance, - missingPermissions = missingPermissions, - ) - } + _uiState.value = State.Permissions( + settingsGuidance = guidance, + missingPermissions = missingPermissions, + ) } } private fun handleNextClicked() { + when (_uiState.value) { + is State.SmsWarning -> handleSmsWarningAcknowledged() + is State.Permissions -> handlePermissionRequest() + } + } + + private fun handleSmsWarningAcknowledged() { + smsWarningStore.acknowledge() + + if (checker.hasRequiredPermissions()) { + emitEffect(Effect.Redirect) + return + } + + _uiState.value = State.Permissions() + } + + private fun handlePermissionRequest() { when (val request = determinePermissionRequest()) { PermissionRequest.SmsRole -> { requestStartedAtMillis = elapsedRealtimeProvider.elapsedRealtimeMillis() diff --git a/src/com/android/messaging/ui/onboarding/screen/model/OnboardingAction.kt b/src/com/android/messaging/ui/onboarding/screen/model/OnboardingAction.kt new file mode 100644 index 000000000..ced6cd72e --- /dev/null +++ b/src/com/android/messaging/ui/onboarding/screen/model/OnboardingAction.kt @@ -0,0 +1,8 @@ +package com.android.messaging.ui.onboarding.screen.model + +internal sealed interface OnboardingAction { + + data object NextClicked : OnboardingAction + + data object SettingsClicked : OnboardingAction +} diff --git a/src/com/android/messaging/ui/onboarding/screen/model/OnboardingScreenEffect.kt b/src/com/android/messaging/ui/onboarding/screen/model/OnboardingScreenEffect.kt new file mode 100644 index 000000000..01ce5a811 --- /dev/null +++ b/src/com/android/messaging/ui/onboarding/screen/model/OnboardingScreenEffect.kt @@ -0,0 +1,16 @@ +package com.android.messaging.ui.onboarding.screen.model + +import kotlinx.collections.immutable.ImmutableList + +internal sealed interface OnboardingScreenEffect { + + data class RequestRuntimePermissions( + val permissions: ImmutableList, + ) : OnboardingScreenEffect + + data object RequestSmsRole : OnboardingScreenEffect + + data object OpenAppSettings : OnboardingScreenEffect + + data object Redirect : OnboardingScreenEffect +} diff --git a/src/com/android/messaging/ui/onboarding/screen/model/OnboardingUiState.kt b/src/com/android/messaging/ui/onboarding/screen/model/OnboardingUiState.kt new file mode 100644 index 000000000..58c01fe2c --- /dev/null +++ b/src/com/android/messaging/ui/onboarding/screen/model/OnboardingUiState.kt @@ -0,0 +1,18 @@ +package com.android.messaging.ui.onboarding.screen.model + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal sealed interface OnboardingUiState { + + @Immutable + data object SmsWarning : OnboardingUiState + + @Immutable + data class Permissions( + val settingsGuidance: SettingsGuidance? = null, + val missingPermissions: ImmutableList = persistentListOf(), + ) : OnboardingUiState +} diff --git a/src/com/android/messaging/ui/onboarding/screen/model/SettingsGuidance.kt b/src/com/android/messaging/ui/onboarding/screen/model/SettingsGuidance.kt new file mode 100644 index 000000000..f604cb51b --- /dev/null +++ b/src/com/android/messaging/ui/onboarding/screen/model/SettingsGuidance.kt @@ -0,0 +1,6 @@ +package com.android.messaging.ui.onboarding.screen.model + +internal enum class SettingsGuidance { + DefaultSmsApp, + Permissions, +} diff --git a/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckAction.kt b/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckAction.kt deleted file mode 100644 index cfd7740f1..000000000 --- a/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckAction.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.android.messaging.ui.permissioncheck.screen.model - -internal sealed interface PermissionCheckAction { - - data object NextClicked : PermissionCheckAction - - data object SettingsClicked : PermissionCheckAction -} diff --git a/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckScreenEffect.kt b/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckScreenEffect.kt deleted file mode 100644 index dc18e4b6d..000000000 --- a/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckScreenEffect.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.messaging.ui.permissioncheck.screen.model - -import kotlinx.collections.immutable.ImmutableList - -internal sealed interface PermissionCheckScreenEffect { - - data class RequestRuntimePermissions( - val permissions: ImmutableList, - ) : PermissionCheckScreenEffect - - data object RequestSmsRole : PermissionCheckScreenEffect - - data object OpenAppSettings : PermissionCheckScreenEffect - - data object Redirect : PermissionCheckScreenEffect -} diff --git a/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckUiState.kt b/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckUiState.kt deleted file mode 100644 index 5c1fa9d56..000000000 --- a/src/com/android/messaging/ui/permissioncheck/screen/model/PermissionCheckUiState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.messaging.ui.permissioncheck.screen.model - -import androidx.compose.runtime.Immutable -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Immutable -internal data class PermissionCheckUiState( - val settingsGuidance: SettingsGuidance? = null, - val missingPermissions: ImmutableList = persistentListOf(), -) - -internal enum class SettingsGuidance { - DefaultSmsApp, - Permissions, -} diff --git a/src/com/android/messaging/util/BuglePrefsKeys.java b/src/com/android/messaging/util/BuglePrefsKeys.java index a0ebb9645..0c1601ac0 100644 --- a/src/com/android/messaging/util/BuglePrefsKeys.java +++ b/src/com/android/messaging/util/BuglePrefsKeys.java @@ -61,6 +61,14 @@ private BuglePrefsKeys() {} // do not instantiate public static final String CONVERSATION_NOTIFICATION_CHANNELS_MIGRATED = "conversation_notification_channels_migrated_v1"; + /** + * The app version code for which the SMS insecurity warning was last acknowledged. + */ + public static final String SMS_WARNING_ACKNOWLEDGED_VERSION + = "sms_warning_acknowledged_version"; + public static final int SMS_WARNING_ACKNOWLEDGED_VERSION_DEFAULT + = BuglePrefs.NO_SHARED_PREFERENCES_VERSION; + /** * The last selected chooser index in the media picker. */ diff --git a/src/com/android/messaging/util/UiUtils.java b/src/com/android/messaging/util/UiUtils.java index 086b00eb2..187250d2f 100644 --- a/src/com/android/messaging/util/UiUtils.java +++ b/src/com/android/messaging/util/UiUtils.java @@ -42,8 +42,11 @@ import com.android.messaging.Factory; import com.android.messaging.R; +import com.android.messaging.domain.onboarding.usecase.ShouldShowOnboarding; import com.android.messaging.ui.UIIntents; +import dagger.hilt.android.EntryPointAccessors; + import java.lang.reflect.Field; import androidx.annotation.Nullable; @@ -259,22 +262,29 @@ public static boolean isRtlMode() { } /** - * Check if the activity needs to be redirected to permission check + * Check if the activity needs to be redirected to onboarding, which covers both the + * once-per-version SMS warning and the permission check. * @return true if {@link Activity#finish()} was called because redirection was performed */ - public static boolean redirectToPermissionCheckIfNeeded(final Activity activity) { - if (!OsUtil.hasRequiredPermissions()) { - UIIntents.get().launchPermissionCheckActivity(activity); - } else { + public static boolean redirectToOnboardingIfNeeded(final Activity activity) { + if (!shouldShowOnboarding()) { // No redirect performed return false; } - // Redirect performed + UIIntents.get().launchOnboardingActivity(activity); activity.finish(); return true; } + private static boolean shouldShowOnboarding() { + final Context context = Factory.get().getApplicationContext(); + return EntryPointAccessors + .fromApplication(context, ShouldShowOnboarding.Provider.class) + .shouldShowOnboarding() + .invoke(); + } + /** * Called to check if all conditions are nominal and a "go" for some action, such as deleting * a message, that requires this app to be the default app. This is also a precondition diff --git a/src/com/android/messaging/util/core/AppVersionProvider.kt b/src/com/android/messaging/util/core/AppVersionProvider.kt new file mode 100644 index 000000000..9b7800790 --- /dev/null +++ b/src/com/android/messaging/util/core/AppVersionProvider.kt @@ -0,0 +1,5 @@ +package com.android.messaging.util.core + +internal fun interface AppVersionProvider { + fun versionCode(): Int +}