diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 942ebee8..c323c528 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -83,6 +83,10 @@ android:name=".presentation.main.notification.NotificationActivity" android:exported="false" android:screenOrientation="portrait" /> + (R.layout.fragment_ private fun addListener() { initEditNicknameButtonClickListener() initMyFeedButtonClickListener() + initSettingButtonClickListener() registerBackPressedCallback() } private fun addObserver() { setupGetUserState() - setupDeleteUserState() + + checkFromWineyFeed() } private fun initCheckNotificationPermission() { @@ -90,44 +89,6 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } } - private fun navigateToNotificationSetting(context: Context) { - val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setNotificationIntentActionOreo(context) - } else { - setNorificationIntentActionOreoLess(context) - } - try { - context.startActivity(intent) - } catch (e: ActivityNotFoundException) { - e.printStackTrace() - } - } - - private fun setNotificationIntentActionOreo(context: Context): Intent { - return Intent().also { intent -> - intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS - intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - } - - private fun setNorificationIntentActionOreoLess(context: Context): Intent { - return Intent().also { intent -> - intent.action = "android.settings.APP_NOTIFICATION_SETTINGS" - intent.putExtra("app_package", context.packageName) - intent.putExtra("app_uid", context.applicationInfo?.uid) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - } - - private fun patchUserInfo() { - lifecycleScope.launch { - val data = dataStoreRepository.getUserInfo().first() - val newData = data?.copy(fcmIsAllowed = false) - dataStoreRepository.saveUserInfo(newData) - } - } - // 닉네임 액티비티 갔다가 다시 돌아왔을 때 유저 데이터 갱신하도록 override fun onStart() { super.onStart() @@ -154,6 +115,12 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } } + private fun initSettingButtonClickListener() { + binding.ivMypageSetting.setOnClickListener { + navigateToSettingScreen() + } + } + // 마이페이지 왔다가 다시 알림 화면으로 돌아가도록 private fun registerBackPressedCallback() { val callback = object : OnBackPressedCallback(true) { @@ -193,25 +160,6 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } } - private fun setupDeleteUserState() { - myPageViewModel.deleteUserState.flowWithLifecycle(viewLifeCycle) - .onEach { state -> - when (state) { - is UiState.Success -> { - myPageViewModel.clearDataStore() - navigateToGuideScreen() - } - - is UiState.Failure -> { - snackBar(binding.root) { state.msg } - } - - else -> { - } - } - }.launchIn(viewLifeCycleScope) - } - private fun navigateToGuideScreen() { Intent(requireContext(), GuideActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) @@ -226,6 +174,12 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } } + private fun navigateToSettingScreen() { + Intent(requireContext(), SettingActivity::class.java).apply { + startActivity(this) + } + } + private fun navigateToMyFeedScreen() { Intent(requireContext(), MyFeedActivity::class.java).apply { startActivity(this) @@ -259,11 +213,6 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ amplitudeUtils.logEvent("view_goalsetting") } - private fun showTargetNotOverDialog() { - val dialog = MyPageNotOverDialogFragment() - dialog.show(parentFragmentManager, dialog.tag) - } - private inline fun navigateAndBackStack() { parentFragmentManager.commit { replace(R.id.fcv_main, T::class.simpleName) diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt index 6346e006..6bf730d8 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt @@ -22,9 +22,6 @@ class MyPageViewModel @Inject constructor( private val _deleteUserState = MutableStateFlow>(UiState.Empty) val deleteUserState: StateFlow> = _deleteUserState.asStateFlow() - private val _patchAllowedNotificationState = MutableStateFlow>(UiState.Empty) - val patchAllowedNotificationState: StateFlow> = _patchAllowedNotificationState.asStateFlow() - fun deleteUser() { viewModelScope.launch { authRepository.deleteUser() @@ -44,30 +41,4 @@ class MyPageViewModel @Inject constructor( } } } - - fun patchAllowedNotification(isAllowed: Boolean) { - viewModelScope.launch { - authRepository.patchAllowedNotification(!isAllowed) - .onSuccess { response -> - Timber.d("SUCCESS PATCH ALLOWED NOTI") - _patchAllowedNotificationState.value = UiState.Success(response) - } - .onFailure { t -> - _patchAllowedNotificationState.value = UiState.Failure(t.message.toString()) - - if (t is HttpException) { - Timber.e("HTTP FAIL ALLOWED NOTI : ${t.code()} ${t.message}") - return@onFailure - } - - Timber.e("FAIL ALLOWED NOTI : ${t.message}") - } - } - } - - fun clearDataStore() { - viewModelScope.launch { - dataStoreRepository.clearDataStore() - } - } } diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/setting/SettingActivity.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/setting/SettingActivity.kt new file mode 100644 index 00000000..f6c237ef --- /dev/null +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/setting/SettingActivity.kt @@ -0,0 +1,349 @@ +package org.go.sopt.winey.presentation.main.mypage.setting + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.activity.viewModels +import androidx.core.content.ContextCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.go.sopt.winey.R +import org.go.sopt.winey.databinding.ActivitySettingBinding +import org.go.sopt.winey.domain.entity.UserV2 +import org.go.sopt.winey.domain.repository.DataStoreRepository +import org.go.sopt.winey.presentation.main.MainViewModel +import org.go.sopt.winey.presentation.model.WineyDialogLabel +import org.go.sopt.winey.presentation.onboarding.guide.GuideActivity +import org.go.sopt.winey.presentation.onboarding.login.LoginActivity +import org.go.sopt.winey.util.amplitude.AmplitudeUtils +import org.go.sopt.winey.util.binding.BindingActivity +import org.go.sopt.winey.util.context.snackBar +import org.go.sopt.winey.util.context.stringOf +import org.go.sopt.winey.util.fragment.WineyDialogFragment +import org.go.sopt.winey.util.view.UiState +import org.go.sopt.winey.util.view.setOnSingleClickListener +import javax.inject.Inject + +@AndroidEntryPoint +class SettingActivity : BindingActivity(R.layout.activity_setting) { + private val settingViewModel by viewModels() + private val mainViewModel by viewModels() + + @Inject + lateinit var dataStoreRepository: DataStoreRepository + + @Inject + lateinit var amplitudeUtils: AmplitudeUtils + private var isNotificationPermissionAllowed = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + addListener() + addObserver() + initNotificationPermissionState() + initUserData() + } + + override fun onStart() { + super.onStart() + initNotificationPermissionState() + updateNotificationButtonByPermission() + } + + private fun addListener() { + init1On1ButtonClickListener() + initLogoutButtonClickListener() + initTermsButtonClickListener() + initWithdrawButtonClickListener() + initNotificationPermissionButtonClickListener() + initNotiToggleButtonClickListener() + initBackButtonClickListener() + } + + private fun addObserver() { + setupDeleteUserState() + setupPatchAllowedNotificationState() + setupLogoutState() + } + + private fun switchOnNotification() { + binding.ivSettingAgree.transitionToState(R.id.end, -1) + patchUserInfo() + settingViewModel.patchAllowedNotification(isAllowed = false) + } + + private fun switchOffNotification() { + binding.ivSettingAgree.transitionToState(R.id.start, -1) + patchUserInfo() + settingViewModel.patchAllowedNotification(isAllowed = true) + } + + private fun initNotificationPermissionButtonClickListener() { + binding.llSettingAgreePermissionChange.setOnClickListener { + showSystemNotificationSetting() + } + } + + private fun showSystemNotificationSetting() { + Intent().apply { + action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(this) + } + } + + private fun showNotificationOffConfirmDialog() { + val dialog = WineyDialogFragment.newInstance( + WineyDialogLabel( + stringOf(R.string.notification_off_dialog_title), + stringOf(R.string.notification_off_dialog_subtitle), + stringOf(R.string.notification_off_dialog_negative_button), + stringOf(R.string.notification_off_dialog_positive_button) + ), + handleNegativeButton = {}, + handlePositiveButton = { switchOffNotification() } + ) + dialog.show( + supportFragmentManager, + TAG_NOTIFICATION_OFF_DIALOG + ) + } + + private fun initUserData() { + lifecycleScope.launch { + val data = dataStoreRepository.getUserInfo().first() + if (data != null) { + updateNotificationAllowSwitchState(data) + } + } + } + + private fun updateNotificationAllowSwitchState(data: UserV2) { + if (isNotificationPermissionAllowed) { + binding.ivSettingAgree.isVisible = true + binding.llSettingAgreePermissionChange.isGone = true + binding.tvSettingAgreePermission.isGone = true + when (data.fcmIsAllowed) { + true -> { + binding.ivSettingAgree.transitionToState(R.id.end, 1) + } + + false -> { + binding.ivSettingAgree.transitionToState(R.id.start, 1) + } + } + } else { + binding.ivSettingAgree.isGone = true + binding.llSettingAgreePermissionChange.isVisible = true + binding.tvSettingAgreePermission.isVisible = true + } + } + + private fun updateNotificationButtonByPermission() { + if (isNotificationPermissionAllowed) { + binding.ivSettingAgree.isVisible = true + + binding.llSettingAgreePermissionChange.isGone = true + binding.tvSettingAgreePermission.isGone = true + } else { + binding.ivSettingAgree.isGone = true + + binding.llSettingAgreePermissionChange.isVisible = true + binding.tvSettingAgreePermission.isVisible = true + } + } + + private fun patchUserInfo() { + lifecycleScope.launch { + val data = dataStoreRepository.getUserInfo().first() + val newData = data?.copy(fcmIsAllowed = false) + dataStoreRepository.saveUserInfo(newData) + } + } + + private fun initNotiToggleButtonClickListener() { + binding.ivSettingSwitch.setOnClickListener { + val isAllowed = when (binding.ivSettingAgree.currentState) { + R.id.start -> false + R.id.end -> true + else -> false + } + + if (!isAllowed) { + switchOnNotification() + } else { + showNotificationOffConfirmDialog() + } + } + } + + private fun initNotificationPermissionState() { + isNotificationPermissionAllowed = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } + + private fun init1On1ButtonClickListener() { + binding.clSettingTo1on1.setOnClickListener { + val url = ONE_ON_ONE_URL + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) + } + } + + private fun initTermsButtonClickListener() { + binding.clSettingToTerms.setOnClickListener { + val url = TERMS_URL + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) + } + } + + private fun initBackButtonClickListener() { + binding.ivSettingBack.setOnSingleClickListener { + finish() + } + } + + private fun initLogoutButtonClickListener() { + binding.clSettingLogout.setOnClickListener { + amplitudeUtils.logEvent("click_logout") + val dialog = WineyDialogFragment.newInstance( + WineyDialogLabel( + stringOf(R.string.mypage_logout_dialog_title), + stringOf(R.string.mypage_logout_dialog_subtitle), + stringOf(R.string.mypage_logout_dialog_negative_button), + stringOf(R.string.mypage_logout_dialog_positive_button) + ), + handleNegativeButton = {}, + handlePositiveButton = { mainViewModel.postLogout() } + ) + dialog.show(supportFragmentManager, TAG_LOGOUT_DIALOG) + } + } + + private fun initWithdrawButtonClickListener() { + binding.clSettingWithdraw.setOnClickListener { + val dialog = WineyDialogFragment.newInstance( + WineyDialogLabel( + stringOf(R.string.mypage_withdraw_dialog_title), + stringOf(R.string.mypage_withdraw_dialog_subtitle), + stringOf(R.string.mypage_withdraw_dialog_negative_button), + stringOf(R.string.mypage_withdraw_dialog_positive_button) + ), + handleNegativeButton = { settingViewModel.deleteUser() }, + handlePositiveButton = {} + ) + dialog.show(supportFragmentManager, TAG_WITHDRAW_DIALOG) + } + } + + private fun setupDeleteUserState() { + settingViewModel.deleteUserState.flowWithLifecycle(lifecycle) + .onEach { state -> + when (state) { + is UiState.Success -> { + settingViewModel.clearDataStore() + navigateToGuideScreen() + } + + is UiState.Failure -> { + snackBar(binding.root) { state.msg } + } + + else -> { + } + } + }.launchIn(lifecycleScope) + } + + private fun setupPatchAllowedNotificationState() { + settingViewModel.patchAllowedNotificationState.flowWithLifecycle(lifecycle) + .onEach { state -> + when (state) { + is UiState.Success -> { + when (state.data) { + true -> { + binding.ivSettingAgree.transitionToState(R.id.end, -1) + } + + false -> { + binding.ivSettingAgree.transitionToState(R.id.start, -1) + } + + null -> { + binding.ivSettingAgree.transitionToState(R.id.start, -1) + } + } + } + + else -> {} + } + } + } + + private fun setupLogoutState() { + mainViewModel.logoutState.flowWithLifecycle(lifecycle).onEach { state -> + when (state) { + is UiState.Loading -> { + } + + is UiState.Success -> { + navigateToLoginScreen() + } + + is UiState.Failure -> { + snackBar(binding.root) { state.msg } + } + + is UiState.Empty -> { + } + } + }.launchIn(lifecycleScope) + } + + private fun navigateToLoginScreen() { + Intent(this, LoginActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(this) + finish() + } + } + + private fun navigateToGuideScreen() { + Intent(this, GuideActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(this) + } + } + + companion object { + + private const val ONE_ON_ONE_URL = "https://open.kakao.com/o/s751Susf" + private const val TERMS_URL = + "https://empty-weaver-a9f.notion.site/iney-9dbfe130c7df4fb9a0903481c3e377e6?pvs=4" + + private const val TAG_LOGOUT_DIALOG = "LOGOUT_DIALOG" + private const val TAG_WITHDRAW_DIALOG = "WITHDRAW_DIALOG" + + private const val TAG_NOTIFICATION_OFF_DIALOG = "offNotification" + } +} diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/setting/SettingViewModel.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/setting/SettingViewModel.kt new file mode 100644 index 00000000..f6ae5b5e --- /dev/null +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/setting/SettingViewModel.kt @@ -0,0 +1,75 @@ +package org.go.sopt.winey.presentation.main.mypage.setting + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.go.sopt.winey.domain.repository.AuthRepository +import org.go.sopt.winey.domain.repository.DataStoreRepository +import org.go.sopt.winey.util.view.UiState +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class SettingViewModel @Inject constructor( + private val authRepository: AuthRepository, + private val dataStoreRepository: DataStoreRepository +) : ViewModel() { + + private val _deleteUserState = MutableStateFlow>(UiState.Empty) + val deleteUserState: StateFlow> = _deleteUserState.asStateFlow() + + private val _patchAllowedNotificationState = MutableStateFlow>(UiState.Empty) + val patchAllowedNotificationState: StateFlow> = + _patchAllowedNotificationState.asStateFlow() + + fun deleteUser() { + viewModelScope.launch { + authRepository.deleteUser() + .onSuccess { response -> + Timber.d("SUCCESS DELETE USER") + _deleteUserState.value = UiState.Success(response) + } + .onFailure { t -> + _deleteUserState.value = UiState.Failure(t.message.toString()) + + if (t is HttpException) { + Timber.e("HTTP FAIL DELETE USER: ${t.code()} ${t.message}") + return@onFailure + } + + Timber.e("FAIL DELETE USER: ${t.message}") + } + } + } + + fun patchAllowedNotification(isAllowed: Boolean) { + viewModelScope.launch { + authRepository.patchAllowedNotification(!isAllowed) + .onSuccess { response -> + Timber.d("SUCCESS PATCH ALLOWED NOTI") + _patchAllowedNotificationState.value = UiState.Success(response) + } + .onFailure { t -> + _patchAllowedNotificationState.value = UiState.Failure(t.message.toString()) + + if (t is HttpException) { + Timber.e("HTTP FAIL ALLOWED NOTI : ${t.code()} ${t.message}") + return@onFailure + } + + Timber.e("FAIL ALLOWED NOTI : ${t.message}") + } + } + } + + fun clearDataStore() { + viewModelScope.launch { + dataStoreRepository.clearDataStore() + } + } +} diff --git a/app/src/main/res/layout/activity_setting.xml b/app/src/main/res/layout/activity_setting.xml new file mode 100644 index 00000000..7a62a270 --- /dev/null +++ b/app/src/main/res/layout/activity_setting.xml @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_my_page.xml b/app/src/main/res/layout/fragment_my_page.xml index 8979f5a4..eadbd331 100644 --- a/app/src/main/res/layout/fragment_my_page.xml +++ b/app/src/main/res/layout/fragment_my_page.xml @@ -26,6 +26,7 @@ app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/xml/mypage_noti_agree_motion_scene.xml b/app/src/main/res/xml/mypage_noti_agree_motion_scene.xml index 3a56fd20..835624b8 100644 --- a/app/src/main/res/xml/mypage_noti_agree_motion_scene.xml +++ b/app/src/main/res/xml/mypage_noti_agree_motion_scene.xml @@ -24,12 +24,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="4dp" - motion:layout_constraintBottom_toBottomOf="@+id/iv_mypage_switch" - motion:layout_constraintStart_toStartOf="@+id/iv_mypage_switch" - motion:layout_constraintTop_toTopOf="@+id/iv_mypage_switch"/> + motion:layout_constraintBottom_toBottomOf="@+id/iv_setting_switch" + motion:layout_constraintStart_toStartOf="@+id/iv_setting_switch" + motion:layout_constraintTop_toTopOf="@+id/iv_setting_switch"/> + motion:layout_constraintBottom_toBottomOf="@+id/iv_setting_switch" + motion:layout_constraintEnd_toEndOf="@+id/iv_setting_switch" + motion:layout_constraintTop_toTopOf="@+id/iv_setting_switch" />