From f55d0d0c9de70012d496903e35a81992b81c34cc Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 17:05:00 +0900 Subject: [PATCH 01/17] chore : notification core module base --- core/notifications/.gitignore | 1 + core/notifications/build.gradle.kts | 15 +++++++++++++ .../notifications/ExampleInstrumentedTest.kt | 22 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 4 ++++ .../core/notifications/ExampleUnitTest.kt | 16 ++++++++++++++ settings.gradle.kts | 1 + 6 files changed, 59 insertions(+) create mode 100644 core/notifications/.gitignore create mode 100644 core/notifications/build.gradle.kts create mode 100644 core/notifications/src/androidTest/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleInstrumentedTest.kt create mode 100644 core/notifications/src/main/AndroidManifest.xml create mode 100644 core/notifications/src/test/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleUnitTest.kt diff --git a/core/notifications/.gitignore b/core/notifications/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/notifications/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/notifications/build.gradle.kts b/core/notifications/build.gradle.kts new file mode 100644 index 00000000..e15c08b3 --- /dev/null +++ b/core/notifications/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.dobedobe.android.library) +} + +android { + namespace = "com.chipichipi.dobedobe.core.notifications" +} + +dependencies { + api(projects.core.model) + implementation(projects.core.common) + + compileOnly(platform(libs.androidx.compose.bom)) + implementation(libs.koin.android) +} diff --git a/core/notifications/src/androidTest/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleInstrumentedTest.kt b/core/notifications/src/androidTest/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..a476cc63 --- /dev/null +++ b/core/notifications/src/androidTest/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.chipichipi.dobedobe.core.notifications + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.chipichipi.dobedobe.core.notifications.test", appContext.packageName) + } +} diff --git a/core/notifications/src/main/AndroidManifest.xml b/core/notifications/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/core/notifications/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/notifications/src/test/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleUnitTest.kt b/core/notifications/src/test/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleUnitTest.kt new file mode 100644 index 00000000..9503e373 --- /dev/null +++ b/core/notifications/src/test/kotlin/com/chipichipi/dobedobe/core/notifications/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.chipichipi.dobedobe.core.notifications + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 355bf0c6..9b0a4ac0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,7 @@ include(":core:common") include(":core:database") include(":core:datastore") include(":core:datastore-proto") +include(":core:notifications") include(":feature:dashboard") include(":feature:goal") From 686f1eef8c953e0a04bc5d7d074fa1f02c04aa6d Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 17:05:37 +0900 Subject: [PATCH 02/17] chore : add POST_NOTIFICATIONS permission --- core/notifications/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/notifications/src/main/AndroidManifest.xml b/core/notifications/src/main/AndroidManifest.xml index a5918e68..972f3b97 100644 --- a/core/notifications/src/main/AndroidManifest.xml +++ b/core/notifications/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file From 50368437a7dd4d6fc3f245009643c086165f9357 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 17:07:07 +0900 Subject: [PATCH 03/17] chore : add notification dependency to main --- app/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 53b75c8e..b33ce79f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,7 +37,8 @@ dependencies { implementation(projects.core.data) implementation(projects.core.designsystem) implementation(projects.core.model) - + implementation(projects.core.notifications) + implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity.compose) implementation(libs.androidx.navigation.compose) From 32b57d684a370f47c667deccbfb056b3035724f3 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 17:40:29 +0900 Subject: [PATCH 04/17] feat: Implement base for notification settings update --- .../core/data/repository/UserRepository.kt | 2 ++ .../data/repository/UserRepositoryImpl.kt | 12 ++++++++ .../dobedobe/data/notification_setting.proto | 9 ++++++ .../dobedobe/data/user_preferences.proto | 3 ++ .../datastore/UserPreferencesDataSource.kt | 30 +++++++++++++++++++ .../dobedobe/core/model/UserData.kt | 2 ++ 6 files changed, 58 insertions(+) create mode 100644 core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto diff --git a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt index de2075bd..07477f51 100644 --- a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt +++ b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt @@ -7,4 +7,6 @@ interface UserRepository { val userData: Flow suspend fun completeOnBoarding(): Result + suspend fun setGoalNotificationChecked(checked: Boolean): Result + suspend fun disableSystemNotificationDialog(): Result } diff --git a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt index 5f638079..9a0b5080 100644 --- a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt @@ -15,4 +15,16 @@ internal class UserRepositoryImpl( userPreferencesDataSource.completeOnBoarding() } } + + override suspend fun setGoalNotificationChecked(checked: Boolean): Result { + return runCatching { + userPreferencesDataSource.setGoalNotificationChecked(checked) + } + } + + override suspend fun disableSystemNotificationDialog(): Result { + return runCatching { + userPreferencesDataSource.disableSystemNotificationDialog() + } + } } diff --git a/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto b/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto new file mode 100644 index 00000000..5ca58827 --- /dev/null +++ b/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "com.chipichipi.dobedobe.core.datastore"; +option java_multiple_files = true; + +message NotificationSetting { + bool is_goal_notification_checked = 1; + bool is_system_notification_dialog_disabled = 2; +} diff --git a/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/user_preferences.proto b/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/user_preferences.proto index 8ea2ca72..6b04dfea 100644 --- a/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/user_preferences.proto +++ b/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/user_preferences.proto @@ -1,8 +1,11 @@ syntax = "proto3"; +import "com/chipichipi/dobedobe/data/notification_setting.proto"; + option java_package = "com.chipichipi.dobedobe.core.datastore"; option java_multiple_files = true; message UserPreferences { bool is_onboarding_completed = 1; + NotificationSetting notification_setting = 2; } diff --git a/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt index 584f0e05..f00bfc45 100644 --- a/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt @@ -27,9 +27,39 @@ class UserPreferencesDataSource( Log.e("UserPreferences", "Failed to update preferences", ioException) } } + + suspend fun setGoalNotificationChecked(checked: Boolean) { + try { + preferences.updateData { + it.copy { + notificationSetting = notificationSetting.copy { + isGoalNotificationChecked = checked + } + } + } + } catch (ioException: IOException) { + Log.e("UserPreferences", "Failed to update preferences", ioException) + } + } + + suspend fun disableSystemNotificationDialog() { + try { + preferences.updateData { + it.copy { + notificationSetting = notificationSetting.copy { + isSystemNotificationDialogDisabled = true + } + } + } + } catch (ioException: IOException) { + Log.e("UserPreferences", "Failed to update preferences", ioException) + } + } } private fun UserPreferences.toModel() = UserData( isOnboardingCompleted = isOnboardingCompleted, + isGoalNotificationChecked = notificationSetting.isGoalNotificationChecked, + isSystemNotificationDialogDisabled = notificationSetting.isSystemNotificationDialogDisabled ) diff --git a/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt b/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt index 84ec3551..68248e70 100644 --- a/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt +++ b/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt @@ -2,4 +2,6 @@ package com.chipichipi.dobedobe.core.model data class UserData( val isOnboardingCompleted: Boolean, + val isGoalNotificationChecked: Boolean, + val isSystemNotificationDialogDisabled: Boolean ) From cfa2ffd2d7f452ad42813ebbfe1e02c6907f0604 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 18:34:36 +0900 Subject: [PATCH 05/17] feat : add temporary wrapping structure for dialog --- .../core/designsystem/component/Dialog.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt diff --git a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt new file mode 100644 index 00000000..980893e7 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt @@ -0,0 +1,81 @@ +package com.chipichipi.dobedobe.core.designsystem.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Surface +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme + +@Composable +fun DobeDobeDialog( + onDismissRequest: () -> Unit, + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false + ), + ) { + Surface( + modifier = modifier + .width(253.dp), + shape = RoundedCornerShape(16.dp), + color = Color.White + ) { + Column( + modifier = Modifier.padding( + vertical = 24.dp, + horizontal = 15.dp + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = title, + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(16.dp)) + + content() + } + } + } +} + +@ThemePreviews +@Composable +private fun DobeDobeDialogPreview() { + DobeDobeTheme { + DobeDobeDialog( + onDismissRequest = {}, + title = "TEST" + ) { + Button( + onClick = {} + ) { + Text(text = "TEST") + } + } + } +} From 86b303c9dd16b11e865058d6556e987cdc1d8582 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 18:41:59 +0900 Subject: [PATCH 06/17] feat : Implement dashboard permission request --- feature/dashboard/build.gradle.kts | 2 + .../feature/dashboard/DashboardScreen.kt | 74 ++++++++++++++++++- .../feature/dashboard/DashboardUiState.kt | 1 + .../feature/dashboard/DashboardViewModel.kt | 63 +++++++++++----- gradle/libs.versions.toml | 2 + 5 files changed, 124 insertions(+), 18 deletions(-) diff --git a/feature/dashboard/build.gradle.kts b/feature/dashboard/build.gradle.kts index 9ec56d2c..a63d3168 100644 --- a/feature/dashboard/build.gradle.kts +++ b/feature/dashboard/build.gradle.kts @@ -10,4 +10,6 @@ android { dependencies { implementation(projects.core.data) implementation(projects.feature.goal) + + implementation(libs.accompanist.permission) } diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt index e727c8e0..a375fd8e 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt @@ -1,5 +1,6 @@ package com.chipichipi.dobedobe.feature.dashboard +import android.os.Build import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.foundation.background @@ -8,13 +9,19 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -22,10 +29,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.chipichipi.dobedobe.core.designsystem.component.DobeDobeBottomSheetScaffold +import com.chipichipi.dobedobe.core.designsystem.component.DobeDobeDialog import com.chipichipi.dobedobe.feature.dashboard.component.DashboardCharacter import com.chipichipi.dobedobe.feature.dashboard.component.DashboardPhotoFrameBox import com.chipichipi.dobedobe.feature.dashboard.component.DashboardTopAppBar import com.chipichipi.dobedobe.feature.dashboard.preview.GoalPreviewParameterProvider +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.rememberPermissionState import org.koin.androidx.compose.koinViewModel @Composable @@ -40,6 +51,8 @@ internal fun DashboardRoute( modifier = modifier, onShowSnackbar = onShowSnackbar, uiState = uiState, + setGoalNotificationChecked = viewModel::setGoalNotificationChecked, + disableSystemNotificationDialog = viewModel::disableSystemNotificationDialog, ) } @@ -48,6 +61,8 @@ internal fun DashboardRoute( private fun DashboardScreen( onShowSnackbar: suspend (String, String?) -> Boolean, uiState: DashboardUiState, + setGoalNotificationChecked: (Boolean) -> Unit, + disableSystemNotificationDialog: () -> Unit, modifier: Modifier = Modifier, ) { val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( @@ -86,7 +101,7 @@ private fun DashboardScreen( when (uiState) { is DashboardUiState.Error, is DashboardUiState.Loading, - -> { + -> { CircularProgressIndicator( modifier = Modifier.size(24.dp), ) @@ -96,6 +111,8 @@ private fun DashboardScreen( uiState = uiState, photoFramesState = photoFramesState, innerPadding = innerPadding, + setGoalNotificationChecked = setGoalNotificationChecked, + disableSystemNotificationDialog = disableSystemNotificationDialog, modifier = Modifier.fillMaxSize(), ) } @@ -110,6 +127,8 @@ private fun DashboardBody( uiState: DashboardUiState.Success, photoFramesState: DashboardPhotoFramesState, innerPadding: PaddingValues, + setGoalNotificationChecked: (Boolean) -> Unit, + disableSystemNotificationDialog: () -> Unit, modifier: Modifier = Modifier, ) { SharedTransitionLayout( @@ -138,4 +157,57 @@ private fun DashboardBody( ) } } + + GoalNotificationPermissionEffect( + isSystemNotificationDialogDisabled = uiState.isSystemNotificationDialogDisabled, + setGoalNotificationChecked = setGoalNotificationChecked, + disableSystemNotificationDialog = disableSystemNotificationDialog + ) +} + +@Composable +@OptIn(ExperimentalPermissionsApi::class) +private fun GoalNotificationPermissionEffect( + isSystemNotificationDialogDisabled: Boolean, + setGoalNotificationChecked: (Boolean) -> Unit, + disableSystemNotificationDialog: () -> Unit +) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return + val notificationsPermissionState = rememberPermissionState( + android.Manifest.permission.POST_NOTIFICATIONS, + ) + var showGoalNotificationDialog by remember { mutableStateOf(false) } + + LaunchedEffect(notificationsPermissionState, isSystemNotificationDialogDisabled) { + val status = notificationsPermissionState.status + + if (status is PermissionStatus.Denied + && !status.shouldShowRationale + && !isSystemNotificationDialogDisabled + ) { + showGoalNotificationDialog = true + } + } + + if (showGoalNotificationDialog) { + DobeDobeDialog( + onDismissRequest = { + showGoalNotificationDialog = false + }, + // TODO : 변경 필요 + title = "목표에 대한 알림을 위해\n 권한이 필요합니다." + ) { + Button( + onClick = { + notificationsPermissionState.launchPermissionRequest() + setGoalNotificationChecked(true) + disableSystemNotificationDialog() + showGoalNotificationDialog = false + } + ) { + // TODO : 변경 필요 + Text("확인") + } + } + } } diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt index 19201823..4c3e25ca 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt @@ -7,6 +7,7 @@ sealed interface DashboardUiState { data class Success( val photoState: List, + val isSystemNotificationDialogDisabled: Boolean ) : DashboardUiState data object Error : DashboardUiState diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt index 30c0c8f6..522de4bd 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt @@ -2,14 +2,18 @@ package com.chipichipi.dobedobe.feature.dashboard import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.chipichipi.dobedobe.core.data.repository.UserRepository import com.chipichipi.dobedobe.core.model.DashboardPhoto import com.chipichipi.dobedobe.feature.dashboard.model.DashboardPhotoConfig import com.chipichipi.dobedobe.feature.dashboard.model.DashboardPhotoState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch // TODO : 제거 필요 private val fakeDashboardPhotoState = @@ -20,22 +24,47 @@ private val fakeDashboardPhotoState = ), ) -class DashboardViewModel() : ViewModel() { - val uiState: StateFlow = - fakeDashboardPhotoState.map { photoData -> - val dashboardPhotoStates = DashboardPhotoConfig.entries.map { config -> - val photo = photoData.find { it.id == config.id } - - DashboardPhotoState( - config = config, - url = photo?.url.orEmpty(), - ) - } - DashboardUiState.Success(dashboardPhotoStates) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = DashboardUiState.Loading, +internal class DashboardViewModel( + private val userRepository: UserRepository, +) : ViewModel() { + + private val isSystemNotificationDialogDisabledFlow = userRepository.userData + .map { it.isSystemNotificationDialogDisabled } + .distinctUntilChanged() + + val uiState: StateFlow = combine( + fakeDashboardPhotoState, + isSystemNotificationDialogDisabledFlow + ) { photoState, isSystemNotificationDialogDisabled -> + val dashboardPhotoStates = DashboardPhotoConfig.entries.map { config -> + val photo = photoState.find { it.id == config.id } + + DashboardPhotoState( + config = config, + url = photo?.url.orEmpty(), ) + } + + DashboardUiState.Success( + dashboardPhotoStates, + isSystemNotificationDialogDisabled + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = DashboardUiState.Loading, + ) + + fun setGoalNotificationChecked(checked: Boolean) { + viewModelScope.launch { + userRepository.setGoalNotificationChecked(checked) + } + } + + fun disableSystemNotificationDialog() { + viewModelScope.launch { + userRepository.disableSystemNotificationDialog() + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eccd32cd..e77d4b78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ robolectric = "4.14.1" # google protobuf = "4.29.2" protobufPlugin = "0.9.4" +permission = "0.37.0" # third-party coil = "2.7.0" @@ -95,6 +96,7 @@ androidx-test-truth = { module = "androidx.test.ext:truth", version.ref = "andro robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } # google +accompanist-permission = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "permission" } protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } From d6e07d721a87e53b9d1ae6958c519620b0661c0a Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 18:50:06 +0900 Subject: [PATCH 07/17] feat : add NotificationUtil --- .../core/notifications/NotificationUtil.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt diff --git a/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt new file mode 100644 index 00000000..40f42a0c --- /dev/null +++ b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt @@ -0,0 +1,36 @@ +package com.chipichipi.dobedobe.core.notifications + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import androidx.core.app.NotificationManagerCompat + +internal fun handleNotificationToggle( + context: Context, + checked: Boolean, + onNotificationToggled: (Boolean) -> Unit, +) { + if (checked) { + if (checkSystemNotificationEnabled(context)) { + onNotificationToggled(true) + } else { + openSystemNotificationSetting(context) + } + } else { + onNotificationToggled(false) + } +} + +internal fun checkSystemNotificationEnabled(context: Context) = + NotificationManagerCompat + .from(context) + .areNotificationsEnabled() + +private fun openSystemNotificationSetting(context: Context) { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + + context.startActivity(intent) +} From 73c9e8b9127abcba786f16aa30040bc230efc642 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 19:06:05 +0900 Subject: [PATCH 08/17] chore : add comments --- .../chipichipi/dobedobe/core/designsystem/component/Dialog.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt index 980893e7..e9d2e941 100644 --- a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt +++ b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt @@ -21,6 +21,9 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme +/** + * TODO : Dialog 컴포넌트 단순 Wrapper 임시 처리, 각 상태 디자인 정의 필요 + */ @Composable fun DobeDobeDialog( onDismissRequest: () -> Unit, From c70226834ef450e270578b9fc3305ace17fb99d5 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 19:07:11 +0900 Subject: [PATCH 09/17] refactor : lint --- app/build.gradle.kts | 2 +- .../core/data/repository/UserRepository.kt | 2 ++ .../core/datastore/UserPreferencesDataSource.kt | 2 +- .../core/designsystem/component/Dialog.kt | 16 ++++++++-------- .../chipichipi/dobedobe/core/model/UserData.kt | 2 +- .../feature/dashboard/DashboardScreen.kt | 16 ++++++++-------- .../feature/dashboard/DashboardUiState.kt | 2 +- .../feature/dashboard/DashboardViewModel.kt | 5 ++--- 8 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b33ce79f..da07b077 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(projects.core.designsystem) implementation(projects.core.model) implementation(projects.core.notifications) - + implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity.compose) implementation(libs.androidx.navigation.compose) diff --git a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt index 07477f51..8e4dc9b3 100644 --- a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt +++ b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt @@ -7,6 +7,8 @@ interface UserRepository { val userData: Flow suspend fun completeOnBoarding(): Result + suspend fun setGoalNotificationChecked(checked: Boolean): Result + suspend fun disableSystemNotificationDialog(): Result } diff --git a/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt index f00bfc45..6b2bc41d 100644 --- a/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt @@ -61,5 +61,5 @@ private fun UserPreferences.toModel() = UserData( isOnboardingCompleted = isOnboardingCompleted, isGoalNotificationChecked = notificationSetting.isGoalNotificationChecked, - isSystemNotificationDialogDisabled = notificationSetting.isSystemNotificationDialogDisabled + isSystemNotificationDialogDisabled = notificationSetting.isSystemNotificationDialogDisabled, ) diff --git a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt index e9d2e941..874a5206 100644 --- a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt +++ b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt @@ -29,33 +29,33 @@ fun DobeDobeDialog( onDismissRequest: () -> Unit, title: String, modifier: Modifier = Modifier, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Dialog( onDismissRequest = onDismissRequest, properties = DialogProperties( - usePlatformDefaultWidth = false + usePlatformDefaultWidth = false, ), ) { Surface( modifier = modifier .width(253.dp), shape = RoundedCornerShape(16.dp), - color = Color.White + color = Color.White, ) { Column( modifier = Modifier.padding( vertical = 24.dp, - horizontal = 15.dp + horizontal = 15.dp, ), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = title, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, - color = Color.Black + color = Color.Black, ) Spacer(modifier = Modifier.height(16.dp)) @@ -72,10 +72,10 @@ private fun DobeDobeDialogPreview() { DobeDobeTheme { DobeDobeDialog( onDismissRequest = {}, - title = "TEST" + title = "TEST", ) { Button( - onClick = {} + onClick = {}, ) { Text(text = "TEST") } diff --git a/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt b/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt index 68248e70..ad6a9507 100644 --- a/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt +++ b/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt @@ -3,5 +3,5 @@ package com.chipichipi.dobedobe.core.model data class UserData( val isOnboardingCompleted: Boolean, val isGoalNotificationChecked: Boolean, - val isSystemNotificationDialogDisabled: Boolean + val isSystemNotificationDialogDisabled: Boolean, ) diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt index a375fd8e..396bacb9 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt @@ -101,7 +101,7 @@ private fun DashboardScreen( when (uiState) { is DashboardUiState.Error, is DashboardUiState.Loading, - -> { + -> { CircularProgressIndicator( modifier = Modifier.size(24.dp), ) @@ -161,7 +161,7 @@ private fun DashboardBody( GoalNotificationPermissionEffect( isSystemNotificationDialogDisabled = uiState.isSystemNotificationDialogDisabled, setGoalNotificationChecked = setGoalNotificationChecked, - disableSystemNotificationDialog = disableSystemNotificationDialog + disableSystemNotificationDialog = disableSystemNotificationDialog, ) } @@ -170,7 +170,7 @@ private fun DashboardBody( private fun GoalNotificationPermissionEffect( isSystemNotificationDialogDisabled: Boolean, setGoalNotificationChecked: (Boolean) -> Unit, - disableSystemNotificationDialog: () -> Unit + disableSystemNotificationDialog: () -> Unit, ) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return val notificationsPermissionState = rememberPermissionState( @@ -181,9 +181,9 @@ private fun GoalNotificationPermissionEffect( LaunchedEffect(notificationsPermissionState, isSystemNotificationDialogDisabled) { val status = notificationsPermissionState.status - if (status is PermissionStatus.Denied - && !status.shouldShowRationale - && !isSystemNotificationDialogDisabled + if (status is PermissionStatus.Denied && + !status.shouldShowRationale && + !isSystemNotificationDialogDisabled ) { showGoalNotificationDialog = true } @@ -195,7 +195,7 @@ private fun GoalNotificationPermissionEffect( showGoalNotificationDialog = false }, // TODO : 변경 필요 - title = "목표에 대한 알림을 위해\n 권한이 필요합니다." + title = "목표에 대한 알림을 위해\n 권한이 필요합니다.", ) { Button( onClick = { @@ -203,7 +203,7 @@ private fun GoalNotificationPermissionEffect( setGoalNotificationChecked(true) disableSystemNotificationDialog() showGoalNotificationDialog = false - } + }, ) { // TODO : 변경 필요 Text("확인") diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt index 4c3e25ca..2654c8ac 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardUiState.kt @@ -7,7 +7,7 @@ sealed interface DashboardUiState { data class Success( val photoState: List, - val isSystemNotificationDialogDisabled: Boolean + val isSystemNotificationDialogDisabled: Boolean, ) : DashboardUiState data object Error : DashboardUiState diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt index 522de4bd..014531f0 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt @@ -27,14 +27,13 @@ private val fakeDashboardPhotoState = internal class DashboardViewModel( private val userRepository: UserRepository, ) : ViewModel() { - private val isSystemNotificationDialogDisabledFlow = userRepository.userData .map { it.isSystemNotificationDialogDisabled } .distinctUntilChanged() val uiState: StateFlow = combine( fakeDashboardPhotoState, - isSystemNotificationDialogDisabledFlow + isSystemNotificationDialogDisabledFlow, ) { photoState, isSystemNotificationDialogDisabled -> val dashboardPhotoStates = DashboardPhotoConfig.entries.map { config -> val photo = photoState.find { it.id == config.id } @@ -47,7 +46,7 @@ internal class DashboardViewModel( DashboardUiState.Success( dashboardPhotoStates, - isSystemNotificationDialogDisabled + isSystemNotificationDialogDisabled, ) } .stateIn( From 764bfd1fa6b7f4bdb9266a30abdae243f1fb1361 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 19:26:10 +0900 Subject: [PATCH 10/17] refactor : NotificationUtil --- .../core/notifications/NotificationUtil.kt | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt index 40f42a0c..15d330ae 100644 --- a/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt +++ b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt @@ -5,32 +5,34 @@ import android.content.Intent import android.provider.Settings import androidx.core.app.NotificationManagerCompat -internal fun handleNotificationToggle( - context: Context, - checked: Boolean, - onNotificationToggled: (Boolean) -> Unit, -) { - if (checked) { - if (checkSystemNotificationEnabled(context)) { - onNotificationToggled(true) +object NotificationUtil { + fun handleNotificationToggle( + context: Context, + checked: Boolean, + onNotificationToggled: (Boolean) -> Unit, + ) { + if (checked) { + if (checkSystemNotificationEnabled(context)) { + onNotificationToggled(true) + } else { + openSystemNotificationSetting(context) + } } else { - openSystemNotificationSetting(context) + onNotificationToggled(false) } - } else { - onNotificationToggled(false) } -} -internal fun checkSystemNotificationEnabled(context: Context) = - NotificationManagerCompat - .from(context) - .areNotificationsEnabled() + fun checkSystemNotificationEnabled(context: Context) = + NotificationManagerCompat + .from(context) + .areNotificationsEnabled() -private fun openSystemNotificationSetting(context: Context) { - val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - .apply { - putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - } + private fun openSystemNotificationSetting(context: Context) { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } - context.startActivity(intent) + context.startActivity(intent) + } } From e295f8ab5dcbc66c430b09a48939e8ee35cb28fa Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Sun, 26 Jan 2025 20:22:15 +0900 Subject: [PATCH 11/17] fix : update initial notification permission logic --- .../feature/dashboard/DashboardScreen.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt index 396bacb9..707415ed 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt @@ -36,6 +36,7 @@ import com.chipichipi.dobedobe.feature.dashboard.component.DashboardTopAppBar import com.chipichipi.dobedobe.feature.dashboard.preview.GoalPreviewParameterProvider import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import org.koin.androidx.compose.koinViewModel @@ -178,14 +179,22 @@ private fun GoalNotificationPermissionEffect( ) var showGoalNotificationDialog by remember { mutableStateOf(false) } - LaunchedEffect(notificationsPermissionState, isSystemNotificationDialogDisabled) { + LaunchedEffect(notificationsPermissionState.status, isSystemNotificationDialogDisabled) { + if (isSystemNotificationDialogDisabled) return@LaunchedEffect val status = notificationsPermissionState.status - if (status is PermissionStatus.Denied && - !status.shouldShowRationale && - !isSystemNotificationDialogDisabled - ) { - showGoalNotificationDialog = true + when { + status is PermissionStatus.Denied && !status.shouldShowRationale -> { + showGoalNotificationDialog = true + } + status is PermissionStatus.Denied && status.shouldShowRationale -> { + setGoalNotificationChecked(false) + disableSystemNotificationDialog() + } + status.isGranted -> { + setGoalNotificationChecked(true) + disableSystemNotificationDialog() + } } } @@ -200,8 +209,6 @@ private fun GoalNotificationPermissionEffect( Button( onClick = { notificationsPermissionState.launchPermissionRequest() - setGoalNotificationChecked(true) - disableSystemNotificationDialog() showGoalNotificationDialog = false }, ) { From 0b2ac38751d10d0e18bea9df5b72edf87857fbb1 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Mon, 27 Jan 2025 00:38:35 +0900 Subject: [PATCH 12/17] feat : Add default values and parameterization to DialogProperties --- .../dobedobe/core/designsystem/component/Dialog.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt index 874a5206..b23e58f1 100644 --- a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt +++ b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Dialog.kt @@ -29,13 +29,14 @@ fun DobeDobeDialog( onDismissRequest: () -> Unit, title: String, modifier: Modifier = Modifier, + properties: DialogProperties = DialogProperties( + usePlatformDefaultWidth = false, + ), content: @Composable () -> Unit, ) { Dialog( onDismissRequest = onDismissRequest, - properties = DialogProperties( - usePlatformDefaultWidth = false, - ), + properties = properties, ) { Surface( modifier = modifier From a69b0b4a6714a10986ab069a3438772aa7390f64 Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Mon, 27 Jan 2025 00:38:50 +0900 Subject: [PATCH 13/17] refactor : Rename GoalNotificationPermissionEffect to GoalNotificationPermission --- .../chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt index 707415ed..fc982141 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt @@ -159,7 +159,7 @@ private fun DashboardBody( } } - GoalNotificationPermissionEffect( + GoalNotificationPermission( isSystemNotificationDialogDisabled = uiState.isSystemNotificationDialogDisabled, setGoalNotificationChecked = setGoalNotificationChecked, disableSystemNotificationDialog = disableSystemNotificationDialog, @@ -168,7 +168,7 @@ private fun DashboardBody( @Composable @OptIn(ExperimentalPermissionsApi::class) -private fun GoalNotificationPermissionEffect( +private fun GoalNotificationPermission( isSystemNotificationDialogDisabled: Boolean, setGoalNotificationChecked: (Boolean) -> Unit, disableSystemNotificationDialog: () -> Unit, From f26d40a79c057da90a6df363e76f1df318a6029f Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Mon, 27 Jan 2025 00:46:06 +0900 Subject: [PATCH 14/17] refactor : Rename checkSystemNotificationEnabled -> areNotificationsEnabled --- .../dobedobe/core/notifications/NotificationUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt index 15d330ae..969e9cd6 100644 --- a/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt +++ b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt @@ -12,7 +12,7 @@ object NotificationUtil { onNotificationToggled: (Boolean) -> Unit, ) { if (checked) { - if (checkSystemNotificationEnabled(context)) { + if (areNotificationsEnabled(context)) { onNotificationToggled(true) } else { openSystemNotificationSetting(context) @@ -22,7 +22,7 @@ object NotificationUtil { } } - fun checkSystemNotificationEnabled(context: Context) = + fun areNotificationsEnabled(context: Context) = NotificationManagerCompat .from(context) .areNotificationsEnabled() From b4f9ec3742a786c30223683f72e17953fe57025b Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Mon, 27 Jan 2025 00:50:29 +0900 Subject: [PATCH 15/17] refactor : Rename setGoalNotificationChecked to setGoalNotificationEnabled --- .../core/data/repository/UserRepository.kt | 2 +- .../core/data/repository/UserRepositoryImpl.kt | 4 ++-- .../dobedobe/data/notification_setting.proto | 2 +- .../core/datastore/UserPreferencesDataSource.kt | 6 +++--- .../chipichipi/dobedobe/core/model/UserData.kt | 2 +- .../core/notifications/NotificationUtil.kt | 4 ++-- .../feature/dashboard/DashboardScreen.kt | 16 ++++++++-------- .../feature/dashboard/DashboardViewModel.kt | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt index 8e4dc9b3..1051c082 100644 --- a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt +++ b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepository.kt @@ -8,7 +8,7 @@ interface UserRepository { suspend fun completeOnBoarding(): Result - suspend fun setGoalNotificationChecked(checked: Boolean): Result + suspend fun setGoalNotificationEnabled(enabled: Boolean): Result suspend fun disableSystemNotificationDialog(): Result } diff --git a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt index 9a0b5080..3a982bcc 100644 --- a/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/chipichipi/dobedobe/core/data/repository/UserRepositoryImpl.kt @@ -16,9 +16,9 @@ internal class UserRepositoryImpl( } } - override suspend fun setGoalNotificationChecked(checked: Boolean): Result { + override suspend fun setGoalNotificationEnabled(enabled: Boolean): Result { return runCatching { - userPreferencesDataSource.setGoalNotificationChecked(checked) + userPreferencesDataSource.setGoalNotificationEnabled(enabled) } } diff --git a/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto b/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto index 5ca58827..c929a1d7 100644 --- a/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto +++ b/core/datastore-proto/src/main/proto/com/chipichipi/dobedobe/data/notification_setting.proto @@ -4,6 +4,6 @@ option java_package = "com.chipichipi.dobedobe.core.datastore"; option java_multiple_files = true; message NotificationSetting { - bool is_goal_notification_checked = 1; + bool is_goal_notification_enabled = 1; bool is_system_notification_dialog_disabled = 2; } diff --git a/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt index 6b2bc41d..ab048928 100644 --- a/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/main/kotlin/com/chipichipi/dobedobe/core/datastore/UserPreferencesDataSource.kt @@ -28,12 +28,12 @@ class UserPreferencesDataSource( } } - suspend fun setGoalNotificationChecked(checked: Boolean) { + suspend fun setGoalNotificationEnabled(enabled: Boolean) { try { preferences.updateData { it.copy { notificationSetting = notificationSetting.copy { - isGoalNotificationChecked = checked + isGoalNotificationEnabled = enabled } } } @@ -60,6 +60,6 @@ class UserPreferencesDataSource( private fun UserPreferences.toModel() = UserData( isOnboardingCompleted = isOnboardingCompleted, - isGoalNotificationChecked = notificationSetting.isGoalNotificationChecked, + isGoalNotificationEnabled = notificationSetting.isGoalNotificationEnabled, isSystemNotificationDialogDisabled = notificationSetting.isSystemNotificationDialogDisabled, ) diff --git a/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt b/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt index ad6a9507..aa66245d 100644 --- a/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt +++ b/core/model/src/main/kotlin/com/chipichipi/dobedobe/core/model/UserData.kt @@ -2,6 +2,6 @@ package com.chipichipi.dobedobe.core.model data class UserData( val isOnboardingCompleted: Boolean, - val isGoalNotificationChecked: Boolean, + val isGoalNotificationEnabled: Boolean, val isSystemNotificationDialogDisabled: Boolean, ) diff --git a/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt index 969e9cd6..15c13095 100644 --- a/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt +++ b/core/notifications/src/main/kotlin/com/chipichipi/dobedobe/core/notifications/NotificationUtil.kt @@ -8,10 +8,10 @@ import androidx.core.app.NotificationManagerCompat object NotificationUtil { fun handleNotificationToggle( context: Context, - checked: Boolean, + enabled: Boolean, onNotificationToggled: (Boolean) -> Unit, ) { - if (checked) { + if (enabled) { if (areNotificationsEnabled(context)) { onNotificationToggled(true) } else { diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt index fc982141..4e72647d 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt @@ -52,7 +52,7 @@ internal fun DashboardRoute( modifier = modifier, onShowSnackbar = onShowSnackbar, uiState = uiState, - setGoalNotificationChecked = viewModel::setGoalNotificationChecked, + setGoalNotificationEnabled = viewModel::setGoalNotificationEnabled, disableSystemNotificationDialog = viewModel::disableSystemNotificationDialog, ) } @@ -62,7 +62,7 @@ internal fun DashboardRoute( private fun DashboardScreen( onShowSnackbar: suspend (String, String?) -> Boolean, uiState: DashboardUiState, - setGoalNotificationChecked: (Boolean) -> Unit, + setGoalNotificationEnabled: (Boolean) -> Unit, disableSystemNotificationDialog: () -> Unit, modifier: Modifier = Modifier, ) { @@ -112,7 +112,7 @@ private fun DashboardScreen( uiState = uiState, photoFramesState = photoFramesState, innerPadding = innerPadding, - setGoalNotificationChecked = setGoalNotificationChecked, + setGoalNotificationEnabled = setGoalNotificationEnabled, disableSystemNotificationDialog = disableSystemNotificationDialog, modifier = Modifier.fillMaxSize(), ) @@ -128,7 +128,7 @@ private fun DashboardBody( uiState: DashboardUiState.Success, photoFramesState: DashboardPhotoFramesState, innerPadding: PaddingValues, - setGoalNotificationChecked: (Boolean) -> Unit, + setGoalNotificationEnabled: (Boolean) -> Unit, disableSystemNotificationDialog: () -> Unit, modifier: Modifier = Modifier, ) { @@ -161,7 +161,7 @@ private fun DashboardBody( GoalNotificationPermission( isSystemNotificationDialogDisabled = uiState.isSystemNotificationDialogDisabled, - setGoalNotificationChecked = setGoalNotificationChecked, + setGoalNotificationEnabled = setGoalNotificationEnabled, disableSystemNotificationDialog = disableSystemNotificationDialog, ) } @@ -170,7 +170,7 @@ private fun DashboardBody( @OptIn(ExperimentalPermissionsApi::class) private fun GoalNotificationPermission( isSystemNotificationDialogDisabled: Boolean, - setGoalNotificationChecked: (Boolean) -> Unit, + setGoalNotificationEnabled: (Boolean) -> Unit, disableSystemNotificationDialog: () -> Unit, ) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return @@ -188,11 +188,11 @@ private fun GoalNotificationPermission( showGoalNotificationDialog = true } status is PermissionStatus.Denied && status.shouldShowRationale -> { - setGoalNotificationChecked(false) + setGoalNotificationEnabled(false) disableSystemNotificationDialog() } status.isGranted -> { - setGoalNotificationChecked(true) + setGoalNotificationEnabled(true) disableSystemNotificationDialog() } } diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt index 014531f0..7e0a6a6a 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardViewModel.kt @@ -55,9 +55,9 @@ internal class DashboardViewModel( initialValue = DashboardUiState.Loading, ) - fun setGoalNotificationChecked(checked: Boolean) { + fun setGoalNotificationEnabled(enabled: Boolean) { viewModelScope.launch { - userRepository.setGoalNotificationChecked(checked) + userRepository.setGoalNotificationEnabled(enabled) } } From f0b9a9a42108287b79971c44643a434db28b279e Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Mon, 27 Jan 2025 01:19:26 +0900 Subject: [PATCH 16/17] =?UTF-8?q?Setting=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : Implement base for feature:setting * feat : Connect dashboard and settings navigation * feat : add temporary wrapping structure for Switch * feat : add openPlayStore Util * feat : add SettingTopAppBar component * feat : add SettingRow component * feat : Implement settings feature requirements * refactor : lint * refactor : Rename onSettingClick to navigateToSetting * refactor : Encapsulate settings UI elements within SettingBody * refactor : Rename isSystemNotificationEnabled to updatedSystemNotificationEnabled * refactor : Remove Box and use modifiers for layout --- .../dobedobe/navigation/DobeDobeNavHost.kt | 39 ++--- .../core/designsystem/component/Switch.kt | 47 ++++++ .../feature/dashboard/DashboardScreen.kt | 5 +- .../dashboard/component/DashboardTopAppBar.kt | 6 +- .../navigation/DashboardNavigation.kt | 2 + feature/setting/build.gradle.kts | 1 + .../dobedobe/feature/setting/SettingScreen.kt | 144 ++++++++++++++++++ .../feature/setting/SettingViewModel.kt | 24 ++- .../feature/setting/component/SettingRow.kt | 76 +++++++++ .../setting/component/SettingTopAppBar.kt | 47 ++++++ .../setting/navigation/SettingNavigation.kt | 27 ++++ .../feature/setting/util/SettingUtil.kt | 15 ++ 12 files changed, 405 insertions(+), 28 deletions(-) create mode 100644 core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Switch.kt create mode 100644 feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt create mode 100644 feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingRow.kt create mode 100644 feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingTopAppBar.kt create mode 100644 feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/navigation/SettingNavigation.kt create mode 100644 feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/util/SettingUtil.kt diff --git a/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt b/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt index 56bf36c7..ee093c2c 100644 --- a/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt +++ b/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt @@ -1,42 +1,35 @@ package com.chipichipi.dobedobe.navigation -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import com.chipichipi.dobedobe.feature.dashboard.navigation.DashboardRoute import com.chipichipi.dobedobe.feature.dashboard.navigation.dashboardScreen +import com.chipichipi.dobedobe.feature.setting.navigation.navigateToSetting +import com.chipichipi.dobedobe.feature.setting.navigation.settingScreen import com.chipichipi.dobedobe.ui.DobeDobeAppState @Composable -fun DobeDobeNavHost( +internal fun DobeDobeNavHost( appState: DobeDobeAppState, onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, ) { val navController = appState.navController - Box( - modifier = - Modifier.windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Top, - ), - ), + NavHost( + navController = navController, + startDestination = DashboardRoute, + modifier = modifier, ) { - NavHost( - navController = navController, - startDestination = DashboardRoute, - modifier = modifier, - ) { - dashboardScreen( - onShowSnackbar = onShowSnackbar, - ) - } + dashboardScreen( + onShowSnackbar = onShowSnackbar, + navigateToSetting = navController::navigateToSetting, + ) + + settingScreen( + onShowSnackbar = onShowSnackbar, + navigateToBack = navController::popBackStack, + ) } } diff --git a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Switch.kt b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Switch.kt new file mode 100644 index 00000000..fc64967e --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Switch.kt @@ -0,0 +1,47 @@ +package com.chipichipi.dobedobe.core.designsystem.component + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme + +/** + * TODO : Switch 컴포넌트 단순 Wrapper 임시 처리, 각 상태 디자인 정의 필요 + */ +@Composable +fun DobeDobeSwitch( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, +) { + // TODO : 디자인시스템 나오면 컬러 정의 필요 + Switch( + modifier = modifier, + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + interactionSource = interactionSource, + colors = SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = Color.Red, + uncheckedThumbColor = Color.Gray, + uncheckedTrackColor = Color.Transparent, + ), + ) +} + +@ThemePreviews +@Composable +private fun DobeDobeSwitchPreview() { + DobeDobeTheme { + DobeDobeSwitch( + checked = true, + onCheckedChange = {}, + ) + } +} diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt index 4e72647d..2de2f485 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt @@ -43,6 +43,7 @@ import org.koin.androidx.compose.koinViewModel @Composable internal fun DashboardRoute( onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToSetting: () -> Unit, modifier: Modifier = Modifier, viewModel: DashboardViewModel = koinViewModel(), ) { @@ -54,6 +55,7 @@ internal fun DashboardRoute( uiState = uiState, setGoalNotificationEnabled = viewModel::setGoalNotificationEnabled, disableSystemNotificationDialog = viewModel::disableSystemNotificationDialog, + navigateToSetting = navigateToSetting, ) } @@ -64,6 +66,7 @@ private fun DashboardScreen( uiState: DashboardUiState, setGoalNotificationEnabled: (Boolean) -> Unit, disableSystemNotificationDialog: () -> Unit, + navigateToSetting: () -> Unit, modifier: Modifier = Modifier, ) { val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( @@ -90,7 +93,7 @@ private fun DashboardScreen( // TODO: 기능 추가 필요 DashboardTopAppBar( onEditClick = {}, - onSettingClick = {}, + navigateToSetting = navigateToSetting, ) }, ) { innerPadding -> diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt index 19ad6b2b..960c64a6 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt @@ -22,7 +22,7 @@ import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme @Composable internal fun DashboardTopAppBar( onEditClick: () -> Unit, - onSettingClick: () -> Unit, + navigateToSetting: () -> Unit, modifier: Modifier = Modifier, ) { DobeDobeTopAppBar( @@ -43,7 +43,7 @@ internal fun DashboardTopAppBar( } // TODO: 아이콘 교체 필요 IconButton( - onClick = onSettingClick, + onClick = navigateToSetting, ) { Icon( Icons.AutoMirrored.Filled.AltRoute, @@ -65,7 +65,7 @@ private fun DashboardTopAppBarPreview() { DobeDobeTheme { DashboardTopAppBar( onEditClick = {}, - onSettingClick = {}, + navigateToSetting = {}, ) } } diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt index 74d2ae66..bca04968 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt @@ -16,10 +16,12 @@ fun NavController.navigateToDashboard( fun NavGraphBuilder.dashboardScreen( onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToSetting: () -> Unit, ) { composable { DashboardRoute( onShowSnackbar = onShowSnackbar, + navigateToSetting = navigateToSetting, ) } } diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts index e02383ad..b27602b9 100644 --- a/feature/setting/build.gradle.kts +++ b/feature/setting/build.gradle.kts @@ -9,4 +9,5 @@ android { dependencies { implementation(projects.core.data) + implementation(projects.core.notifications) } diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt new file mode 100644 index 00000000..94359713 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt @@ -0,0 +1,144 @@ +package com.chipichipi.dobedobe.feature.setting + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowForwardIos +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +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.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.chipichipi.dobedobe.core.designsystem.component.DobeDobeSwitch +import com.chipichipi.dobedobe.core.notifications.NotificationUtil +import com.chipichipi.dobedobe.core.notifications.NotificationUtil.checkSystemNotificationEnabled +import com.chipichipi.dobedobe.feature.setting.component.SettingRow +import com.chipichipi.dobedobe.feature.setting.component.SettingTopAppBar +import com.chipichipi.dobedobe.feature.setting.util.openPlayStore +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun SettingRoute( + onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: SettingViewModel = koinViewModel(), +) { + val isGoalNotificationChecked by viewModel.isGoalNotificationChecked.collectAsStateWithLifecycle() + + SettingScreen( + modifier = modifier, + isGoalNotificationChecked = isGoalNotificationChecked, + navigateToBack = navigateToBack, + onNotificationToggled = viewModel::setGoalNotificationChecked, + ) +} + +@Composable +private fun SettingScreen( + isGoalNotificationChecked: Boolean, + navigateToBack: () -> Unit, + onNotificationToggled: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + SettingTopAppBar( + navigateToBack = navigateToBack, + ) + }, + ) { innerPadding -> + SettingBody( + isGoalNotificationChecked = isGoalNotificationChecked, + onNotificationToggled = onNotificationToggled, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) + } + + GoalNotificationEffect( + onNotificationToggled = onNotificationToggled, + ) +} + +@Composable +private fun SettingBody( + isGoalNotificationChecked: Boolean, + onNotificationToggled: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Column( + modifier = modifier, + ) { + // TODO : 언어 대응 필요 + SettingRow( + label = "알림", + ) { + DobeDobeSwitch( + modifier = Modifier.padding(end = 8.dp), + checked = isGoalNotificationChecked, + onCheckedChange = { checked -> + NotificationUtil.handleNotificationToggle( + context = context, + checked = checked, + onNotificationToggled = onNotificationToggled, + ) + }, + ) + } + + // TODO : 언어 대응 필요 + SettingRow( + label = "앱 피드백 남기기", + ) { + IconButton( + modifier = Modifier.size(42.dp), + onClick = { openPlayStore(context) }, + ) { + // TODO: 아이콘 변경 필요 + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.ArrowForwardIos, + contentDescription = null, + ) + } + } + } +} + +@Composable +private fun GoalNotificationEffect( + onNotificationToggled: (Boolean) -> Unit, +) { + val context = LocalContext.current + + var systemNotificationEnabled by remember { + mutableStateOf(checkSystemNotificationEnabled(context)) + } + + LifecycleResumeEffect(Unit) { + val updatedSystemNotificationEnabled = checkSystemNotificationEnabled(context) + + if (systemNotificationEnabled != updatedSystemNotificationEnabled) { + systemNotificationEnabled = updatedSystemNotificationEnabled + if (updatedSystemNotificationEnabled) { + onNotificationToggled(true) + } + } + onPauseOrDispose { } + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt index 9f5196bd..9c09f308 100644 --- a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt @@ -1,5 +1,27 @@ package com.chipichipi.dobedobe.feature.setting import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.chipichipi.dobedobe.core.data.repository.UserRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch -class SettingViewModel : ViewModel() +internal class SettingViewModel( + private val userRepository: UserRepository, +) : ViewModel() { + val isGoalNotificationChecked = userRepository.userData + .map { it.isGoalNotificationChecked } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false, + ) + + fun setGoalNotificationChecked(checked: Boolean) { + viewModelScope.launch { + userRepository.setGoalNotificationChecked(checked) + } + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingRow.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingRow.kt new file mode 100644 index 00000000..780c6870 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingRow.kt @@ -0,0 +1,76 @@ +package com.chipichipi.dobedobe.feature.setting.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.chipichipi.dobedobe.core.designsystem.component.ThemePreviews +import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme + +@Composable +internal fun SettingRow( + label: String, + modifier: Modifier = Modifier, + trailingContent: @Composable () -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(52.dp) + .padding( + start = 24.dp, + end = 8.dp, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + // TODO : 폰트 스타일 변경 필요 + Text( + text = label, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Black, + ) + + trailingContent() + } + + // TODO : 색상 변경 필요 + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 8.dp, + color = Color(0xFFF2F3F6), + ) + } +} + +@ThemePreviews +@Composable +private fun SettingRowPreview() { + DobeDobeTheme { + SettingRow( + label = "TEST", + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = "TEST") + } + } + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingTopAppBar.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingTopAppBar.kt new file mode 100644 index 00000000..a972b3cf --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingTopAppBar.kt @@ -0,0 +1,47 @@ +package com.chipichipi.dobedobe.feature.setting.component + +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.chipichipi.dobedobe.core.designsystem.component.DobeDobeTopAppBar +import com.chipichipi.dobedobe.core.designsystem.component.ThemePreviews +import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingTopAppBar( + navigateToBack: () -> Unit, + modifier: Modifier = Modifier, +) { + DobeDobeTopAppBar( + modifier = modifier, + navigationIcon = { + IconButton( + modifier = Modifier.size(48.dp), + onClick = navigateToBack, + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.ArrowBackIosNew, + contentDescription = null, + ) + } + }, + ) +} + +@ThemePreviews +@Composable +private fun SettingTopAppBarPreview() { + DobeDobeTheme { + SettingTopAppBar( + navigateToBack = {}, + ) + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/navigation/SettingNavigation.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/navigation/SettingNavigation.kt new file mode 100644 index 00000000..3352961f --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/navigation/SettingNavigation.kt @@ -0,0 +1,27 @@ +package com.chipichipi.dobedobe.feature.setting.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.chipichipi.dobedobe.feature.setting.SettingRoute +import kotlinx.serialization.Serializable + +@Serializable +data object SettingRoute + +fun NavController.navigateToSetting( + navOptions: NavOptions? = null, +) = navigate(route = SettingRoute, navOptions) + +fun NavGraphBuilder.settingScreen( + onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToBack: () -> Unit, +) { + composable { + SettingRoute( + onShowSnackbar = onShowSnackbar, + navigateToBack = navigateToBack, + ) + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/util/SettingUtil.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/util/SettingUtil.kt new file mode 100644 index 00000000..bb84ea71 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/util/SettingUtil.kt @@ -0,0 +1,15 @@ +package com.chipichipi.dobedobe.feature.setting.util + +import android.content.Context +import android.content.Intent +import android.net.Uri + +// TODO : 주소 확인 필요 +internal fun openPlayStore(context: Context) { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=com.chipichipi.dobedobe"), + ) + + context.startActivity(intent) +} From b727883bf6ff2fee8235f77fcc538074956a9f0f Mon Sep 17 00:00:00 2001 From: Junhyeok Date: Mon, 27 Jan 2025 01:23:13 +0900 Subject: [PATCH 17/17] refactor : Rename isGoalNotificationChecked to isGoalNotificationEnabled --- .../dobedobe/feature/setting/SettingScreen.kt | 22 +++++++++---------- .../feature/setting/SettingViewModel.kt | 8 +++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt index 94359713..2e5fcbce 100644 --- a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt @@ -21,7 +21,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.chipichipi.dobedobe.core.designsystem.component.DobeDobeSwitch import com.chipichipi.dobedobe.core.notifications.NotificationUtil -import com.chipichipi.dobedobe.core.notifications.NotificationUtil.checkSystemNotificationEnabled +import com.chipichipi.dobedobe.core.notifications.NotificationUtil.areNotificationsEnabled import com.chipichipi.dobedobe.feature.setting.component.SettingRow import com.chipichipi.dobedobe.feature.setting.component.SettingTopAppBar import com.chipichipi.dobedobe.feature.setting.util.openPlayStore @@ -34,19 +34,19 @@ internal fun SettingRoute( modifier: Modifier = Modifier, viewModel: SettingViewModel = koinViewModel(), ) { - val isGoalNotificationChecked by viewModel.isGoalNotificationChecked.collectAsStateWithLifecycle() + val isGoalNotificationEnabled by viewModel.isGoalNotificationEnabled.collectAsStateWithLifecycle() SettingScreen( modifier = modifier, - isGoalNotificationChecked = isGoalNotificationChecked, + isGoalNotificationEnabled = isGoalNotificationEnabled, navigateToBack = navigateToBack, - onNotificationToggled = viewModel::setGoalNotificationChecked, + onNotificationToggled = viewModel::setGoalNotificationEnabled, ) } @Composable private fun SettingScreen( - isGoalNotificationChecked: Boolean, + isGoalNotificationEnabled: Boolean, navigateToBack: () -> Unit, onNotificationToggled: (Boolean) -> Unit, modifier: Modifier = Modifier, @@ -60,7 +60,7 @@ private fun SettingScreen( }, ) { innerPadding -> SettingBody( - isGoalNotificationChecked = isGoalNotificationChecked, + isGoalNotificationEnabled = isGoalNotificationEnabled, onNotificationToggled = onNotificationToggled, modifier = Modifier .fillMaxSize() @@ -75,7 +75,7 @@ private fun SettingScreen( @Composable private fun SettingBody( - isGoalNotificationChecked: Boolean, + isGoalNotificationEnabled: Boolean, onNotificationToggled: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { @@ -90,11 +90,11 @@ private fun SettingBody( ) { DobeDobeSwitch( modifier = Modifier.padding(end = 8.dp), - checked = isGoalNotificationChecked, + checked = isGoalNotificationEnabled, onCheckedChange = { checked -> NotificationUtil.handleNotificationToggle( context = context, - checked = checked, + enabled = checked, onNotificationToggled = onNotificationToggled, ) }, @@ -127,11 +127,11 @@ private fun GoalNotificationEffect( val context = LocalContext.current var systemNotificationEnabled by remember { - mutableStateOf(checkSystemNotificationEnabled(context)) + mutableStateOf(areNotificationsEnabled(context)) } LifecycleResumeEffect(Unit) { - val updatedSystemNotificationEnabled = checkSystemNotificationEnabled(context) + val updatedSystemNotificationEnabled = areNotificationsEnabled(context) if (systemNotificationEnabled != updatedSystemNotificationEnabled) { systemNotificationEnabled = updatedSystemNotificationEnabled diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt index 9c09f308..f3d11faa 100644 --- a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt @@ -11,17 +11,17 @@ import kotlinx.coroutines.launch internal class SettingViewModel( private val userRepository: UserRepository, ) : ViewModel() { - val isGoalNotificationChecked = userRepository.userData - .map { it.isGoalNotificationChecked } + val isGoalNotificationEnabled = userRepository.userData + .map { it.isGoalNotificationEnabled } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = false, ) - fun setGoalNotificationChecked(checked: Boolean) { + fun setGoalNotificationEnabled(checked: Boolean) { viewModelScope.launch { - userRepository.setGoalNotificationChecked(checked) + userRepository.setGoalNotificationEnabled(checked) } } }